diff options
729 files changed, 13005 insertions, 7737 deletions
diff --git a/.travis.yml b/.travis.yml index 9d8632f9b0..605d1ff247 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,7 @@ language: ruby sudo: false script: 'ci/travis.rb' before_install: - - gem uninstall bundler -x --force - - gem install bundler --version "< 1.10" + - gem install bundler - "rm ${BUNDLE_GEMFILE}.lock" before_script: - bundle update @@ -20,17 +19,14 @@ env: - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - "GEM=aj:integration" + - "GEM=guides" rvm: - 2.2.2 - ruby-head - - rbx-2 - - jruby-head matrix: allow_failures: - env: "GEM=ar:mysql" - rvm: ruby-head - - rvm: rbx-2 - - rvm: jruby-head fast_finish: true notifications: email: false @@ -5,6 +5,9 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem 'rake', '>= 10.3' +# Active Job depends on the URI::GID::MissingModelIDError, which isn't released yet. +gem 'globalid', github: 'rails/globalid' + # This needs to be with require false as it is # loaded after loading the test library to # ensure correct loading order @@ -46,7 +49,7 @@ group :job do gem 'sidekiq', require: false gem 'sucker_punch', require: false gem 'delayed_job', require: false - gem 'queue_classic', require: false, platforms: :ruby + gem 'queue_classic', github: "QueueClassic/queue_classic", require: false, platforms: :ruby gem 'sneakers', require: false gem 'que', require: false gem 'backburner', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5adbf65045..52e221c6fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,4 +1,11 @@ GIT + remote: git://github.com/QueueClassic/queue_classic.git + revision: d144db29f1436e9e8b3c7a1a1ecd4442316a9ecd + specs: + queue_classic (3.2.0.alpha) + pg (>= 0.17, < 0.19) + +GIT remote: git://github.com/bkeepers/qu.git revision: d098e2657c92e89a6413bebd9c033930759c061f branch: master @@ -13,37 +20,44 @@ GIT GIT remote: git://github.com/mikel/mail.git - revision: b159e0a542962fdd5e292a48cfffa560d7cf412e + revision: 64ef1a12efcdda53fd63e1456c2c564044bf82ce specs: mail (2.6.3.edge) mime-types (>= 1.16, < 3) GIT remote: git://github.com/rails/arel.git - revision: aac9da257f291ad8d2d4f914528881c240848bb2 + revision: d5432b4616ff43fbb14540d351eed351e21bb20e branch: master specs: arel (7.0.0.alpha) GIT + remote: git://github.com/rails/globalid.git + revision: 8178ff2dc898a8f49dd71f6eb46f2a09712462de + specs: + globalid (0.3.6) + activesupport (>= 4.1.0) + +GIT remote: git://github.com/rails/jquery-rails.git - revision: 272abdd319bb3381b23182b928b25320590096b0 + revision: 38053f45402f1ccc4cce5dd8c7005ec707376db3 branch: master specs: - jquery-rails (4.0.3) + jquery-rails (4.0.4) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) GIT remote: git://github.com/rails/sprockets-rails.git - revision: 85b89c44ad40af3056899808475e6e4bf65c1f5a + revision: ad4a43bd1bb19c86a8bf94a2ad5e477686161490 branch: master specs: - sprockets-rails (3.0.0.beta1) + sprockets-rails (3.0.0) actionpack (>= 4.0) activesupport (>= 4.0) - sprockets (>= 3.0.0, < 4.0) + sprockets (>= 3.0.0) PATH remote: . @@ -78,8 +92,10 @@ PATH activesupport (= 5.0.0.alpha) arel (= 7.0.0.alpha) activesupport (5.0.0.alpha) + concurrent-ruby (~> 0.9.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) + method_source minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) @@ -105,75 +121,68 @@ GEM remote: https://rubygems.org/ specs: amq-protocol (1.9.2) - backburner (0.4.6) - beaneater (~> 0.3.1) - dante (~> 0.1.5) + backburner (1.0.0) + beaneater (~> 1.0) + dante (> 0.1.5) bcrypt (3.1.10) bcrypt (3.1.10-x64-mingw32) bcrypt (3.1.10-x86-mingw32) - beaneater (0.3.3) - benchmark-ips (2.1.1) + beaneater (1.0.0) + benchmark-ips (2.3.0) builder (3.2.2) bunny (1.7.0) amq-protocol (>= 1.9.2) - byebug (4.0.5) + byebug (5.0.0) columnize (= 0.9.0) - celluloid (0.16.0) + celluloid (0.16.1) timers (~> 4.0.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.3.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.0) + coffee-script-source (1.9.1.1) columnize (0.9.0) - connection_pool (2.1.1) - dalli (2.7.2) - dante (0.1.5) + concurrent-ruby (0.9.1) + connection_pool (2.2.0) + dalli (2.7.4) + dante (0.2.0) delayed_job (4.0.6) activesupport (>= 3.0, < 5.0) delayed_job_active_record (4.0.3) activerecord (>= 3.0, < 5.0) delayed_job (>= 3.0, < 4.1) erubis (2.7.0) - execjs (2.3.0) - globalid (0.3.3) - activesupport (>= 4.1.0) + execjs (2.5.2) hitimes (1.2.2) hitimes (1.2.2-x86-mingw32) i18n (0.7.0) - json (1.8.2) + json (1.8.3) kindlerb (0.1.1) mustache nokogiri - loofah (2.0.1) + loofah (2.0.2) nokogiri (>= 1.5.9) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.4.3) + mime-types (2.6.1) mini_portile (0.6.2) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - multi_json (1.11.0) - mustache (1.0.0) + multi_json (1.11.2) + mustache (1.0.2) mysql (2.9.1) - mysql2 (0.3.18) + mysql2 (0.3.19) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - nokogiri (1.6.6.2-x64-mingw32) - mini_portile (~> 0.6.0) - nokogiri (1.6.6.2-x86-mingw32) - mini_portile (~> 0.6.0) - pg (0.18.1) + pg (0.18.2) psych (2.0.13) - que (0.9.2) - queue_classic (3.1.0) - pg (>= 0.17, < 0.19) + que (0.10.0) racc (1.4.12) - rack (1.6.0) + rack (1.6.4) rack-cache (1.2) rack (>= 0.4) rack-protection (1.5.3) @@ -192,7 +201,7 @@ GEM rdoc (4.2.0) redcarpet (3.2.3) redis (3.2.1) - redis-namespace (1.5.1) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.25.2) mono_logger (~> 1.0) @@ -205,47 +214,46 @@ GEM redis (~> 3.0) resque (~> 1.25) rufus-scheduler (~> 3.0) - rufus-scheduler (3.0.9) - tzinfo + rufus-scheduler (3.1.3) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sequel (4.19.0) + sequel (4.25.0) serverengine (1.5.10) sigdump (~> 0.2.2) - sidekiq (3.3.2) - celluloid (>= 0.16.0) - connection_pool (>= 2.1.1) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) - sigdump (0.2.2) - sinatra (1.4.5) + sidekiq (3.4.2) + celluloid (~> 0.16.0) + connection_pool (~> 2.2, >= 2.2.0) + json (~> 1.0) + redis (~> 3.2, >= 3.2.1) + redis-namespace (~> 1.5, >= 1.5.2) + sigdump (0.2.3) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) sneakers (1.0.4) bunny (~> 1.7.0) serverengine (~> 1.5.5) thor thread (~> 0.1.7) - sprockets (3.0.2) + sprockets (3.0.3) rack (~> 1.0) sqlite3 (1.3.10) stackprof (0.2.7) - sucker_punch (1.3.2) + sucker_punch (1.5.0) celluloid (~> 0.16.0) thor (0.19.1) thread (0.1.7) thread_safe (0.3.5) - tilt (1.4.1) + tilt (2.0.1) timers (4.0.1) hitimes turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.7.0) + uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) vegas (0.1.11) @@ -272,6 +280,7 @@ DEPENDENCIES dalli (>= 2.2.1) delayed_job delayed_job_active_record + globalid! jquery-rails! json kindlerb (= 0.1.1) @@ -286,7 +295,7 @@ DEPENDENCIES qu-rails! qu-redis que - queue_classic + queue_classic! racc (>= 1.4.6) rack-cache (~> 1.2) rails! @@ -306,3 +315,6 @@ DEPENDENCIES turbolinks uglifier (>= 1.3.0) w3c_validators + +BUNDLED WITH + 1.10.5 diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.md index a020b958e1..faf8fa7f0d 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.md @@ -1,43 +1,47 @@ -= Releasing Rails +# Releasing Rails In this document, we'll cover the steps necessary to release Rails. Each section contains steps to take during that time before the release. The times suggested in each header are just that: suggestions. However, they should really be considered as minimums. -== 10 Days before release +## 10 Days before release Today is mostly coordination tasks. Here are the things you must do today: -=== Is the CI green? If not, make it green. (See "Fixing the CI") +### Is the CI green? If not, make it green. (See "Fixing the CI") Do not release with a Red CI. You can find the CI status here: - http://travis-ci.org/rails/rails +``` +http://travis-ci.org/rails/rails +``` -=== Is Sam Ruby happy? If not, make him happy. +### Is Sam Ruby happy? If not, make him happy. Sam Ruby keeps a test suite that makes sure the code samples in his book (Agile Web Development with Rails) all work. These are valuable integration tests for Rails. You can check the status of his tests here: - http://intertwingly.net/projects/dashboard.html +``` +http://intertwingly.net/projects/dashboard.html +``` Do not release with Red AWDwR tests. -=== Do we have any Git dependencies? If so, contact those authors. +### Do we have any Git dependencies? If so, contact those authors. Having Git dependencies indicates that we depend on unreleased code. Obviously Rails cannot be released when it depends on unreleased code. Contact the authors of those particular gems and work out a release date that suits them. -=== Contact the security team (either Koz or tenderlove) +### Contact the security team (either Koz or tenderlove) Let them know of your plans to release. There may be security issues to be addressed, and that can impact your release date. -=== Notify implementors. +### Notify implementors. Ruby implementors have high stakes in making sure Rails works. Be kind and give them a heads up that Rails will be released soonish. @@ -54,27 +58,29 @@ 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: -=== Is the CI green? If not, make it green. +### Is the CI green? If not, make it green. -=== Is Sam Ruby happy? If not, make him happy. +### Is Sam Ruby happy? If not, make him happy. -=== Contact the security team. CVE emails must be sent on this day. +### Contact the security team. CVE emails must be sent on this day. -=== Create a release branch. +### Create a release branch. From the stable branch, create a release branch. For example, if you're releasing Rails 3.0.10, do this: - [aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 - Switched to a new branch '3-0-10' - [aaron@higgins rails (3-0-10)]$ +``` +[aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 +Switched to a new branch '3-0-10' +[aaron@higgins rails (3-0-10)]$ +``` -=== Update each CHANGELOG. +### Update each CHANGELOG. Many times commits are made without the CHANGELOG being updated. You should review the commits since the last release, and fill in any missing information @@ -82,15 +88,17 @@ for each CHANGELOG. You can review the commits for the 3.0.10 release like this: - [aaron@higgins rails (3-0-10)]$ git log v3.0.9.. +``` +[aaron@higgins rails (3-0-10)]$ git log v3.0.9.. +``` If you're doing a stable branch release, you should also ensure that all of the CHANGELOG entries in the stable branch are also synced to the master branch. -=== Update the RAILS_VERSION file to include the RC. +### Update the RAILS_VERSION file to include the RC. -=== Build and test the gem. +### Build and test the gem. Run `rake install` to generate the gems and install them locally. Then try generating a new app and ensure that nothing explodes. @@ -98,7 +106,7 @@ generating a new app and ensure that nothing explodes. This will stop you from looking silly when you push an RC to rubygems.org and then realize it is broken. -=== Release the gem. +### Release the gem. IMPORTANT: Due to YAML parse problems on the rubygems.org server, it is safest to use Ruby 1.8 when releasing. @@ -108,14 +116,16 @@ RAILS_VERSION, commit the changes, tag it, and push the gems to rubygems.org. Here are the commands that `rake release` should use, so you can understand what to do in case anything goes wrong: - $ rake all:build - $ git commit -am'updating RAILS_VERSION' - $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 - $ git push - $ git push --tags - $ for i in $(ls pkg); do gem push $i; done +``` +$ rake all:build +$ git commit -am'updating RAILS_VERSION' +$ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 +$ git push +$ git push --tags +$ for i in $(ls pkg); do gem push $i; done +``` -=== Send Rails release announcements +### Send Rails release announcements Write a release announcement that includes the version, changes, and links to GitHub where people can find the specific commit list. Here are the mailing @@ -132,16 +142,16 @@ IMPORTANT: If any users experience regressions when using the release candidate, you *must* postpone the release. Bugfix releases *should not* break existing applications. -=== Post the announcement to the Rails blog. +### Post the announcement to the Rails blog. If you used Markdown format for your email, you can just paste it in to the blog. * http://weblog.rubyonrails.org -=== Post the announcement to the Rails Twitter account. +### Post the announcement to the Rails Twitter account. -== Time between release candidate and actual release +## Time between release candidate and actual release Check the rails-core mailing list and the GitHub issue list for regressions in the RC. @@ -155,7 +165,7 @@ When you fix the regressions, do not create a new branch. Fix them on the stable branch, then cherry pick the commit to your release branch. No other commits should be added to the release branch besides regression fixing commits. -== Day of release +## Day of release Many of these steps are the same as for the release candidate, so if you need more explanation on a particular step, see the RC steps. @@ -173,7 +183,7 @@ Today, do this stuff in this order: * Email security lists * Email general announcement lists -=== Emailing the Rails security announce list +### Emailing the Rails security announce list Email the security announce list once for each vulnerability fixed. @@ -193,13 +203,13 @@ so we need to give them the security fixes in patch form. * Merge the release branch to the stable branch. * Drink beer (or other cocktail) -== Misc +## Misc -=== Fixing the CI +### Fixing the CI There are two simple steps for fixing the CI: 1. Identify the problem 2. Fix it -Repeat these steps until the CI is green. +Repeat these steps until the CI is green.
\ No newline at end of file diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index acdc4c7dc9..7c1b0b215a 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add `config.action_mailer.deliver_later_queue_name` configuration to set the + mailer queue name. + + *Chris McGrath* + * `assert_emails` in block form use the given number as expected value. This makes the error message much easier to understand. diff --git a/actionmailer/bin/test b/actionmailer/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionmailer/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 4a5229fe18..6f49e130d8 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -155,7 +155,7 @@ module ActionMailer # Note that <tt>deliver_later</tt> will execute your method from the background job. # # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. - # All instance method are expected to return a message object to be sent. + # All instance methods are expected to return a message object to be sent. # # = Multipart Emails # @@ -413,6 +413,8 @@ 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> class Base < AbstractController::Base include DeliveryMethods include Previews @@ -818,7 +820,7 @@ module ActionMailer # Set configure delivery behavior wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options)) - # Assign all headers except parts_order, content_type and body + # Assign all headers except parts_order, content_type, body, template_name, and template_path assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) assignable.each { |k, v| m[k] = v } diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb index e864ab7a4d..52772af2d3 100644 --- a/actionmailer/lib/action_mailer/delivery_job.rb +++ b/actionmailer/lib/action_mailer/delivery_job.rb @@ -4,7 +4,7 @@ module ActionMailer # The <tt>ActionMailer::DeliveryJob</tt> class is used when you # want to send emails outside of the request-response cycle. class DeliveryJob < ActiveJob::Base # :nodoc: - queue_as :mailers + queue_as { ActionMailer::Base.deliver_later_queue_name } def perform(mailer, mail_method, delivery_method, *args) #:nodoc: mailer.constantize.public_send(mail_method, *args).send(delivery_method) diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index aedcd81e52..4758b55a2a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -16,6 +16,9 @@ module ActionMailer cattr_accessor :perform_deliveries self.perform_deliveries = true + cattr_accessor :deliver_later_queue_name + self.deliver_later_queue_name = :mailers + self.delivery_methods = {}.freeze self.delivery_method = :smtp diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index dea5e52f0c..6d02b39225 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -5,7 +5,7 @@ module ActionMailer # that use inline cid: style urls to data: style urls so that they are visible # when previewing a HTML email in a web browser. # - # This interceptor is enabled by default, to remove it delete it from the + # This interceptor is enabled by default. To disable it, delete it from the # <tt>ActionMailer::Base.preview_interceptors</tt> array: # # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 5b57c75ec3..c2f671fdac 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -2,7 +2,7 @@ require 'active_support/log_subscriber' module ActionMailer # Implements the ActiveSupport::LogSubscriber for logging notifications when - # email is delivered and received. + # email is delivered or received. class LogSubscriber < ActiveSupport::LogSubscriber # An email was delivered. def deliver(event) diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index ae91903c95..706249a2f6 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -11,7 +11,6 @@ end require 'active_support/testing/autorun' require 'action_mailer' require 'action_mailer/test_case' -require 'mail' # Emulate AV railtie require 'action_view' diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 010e44d045..5ffde06a80 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'action_view' require 'action_controller' +require 'active_support/deprecation' class I18nTestMailer < ActionMailer::Base configure do |c| @@ -54,7 +55,9 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest def test_send_mail Mail::SMTP.any_instance.expects(:deliver!) with_translation 'de', email_subject: '[Anmeldung] Willkommen' do - get '/test/send_mail' + ActiveSupport::Deprecation.silence do + get '/test/send_mail' + end assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body end end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index aaa3f218b5..862ce26187 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -9,6 +9,8 @@ class MessageDeliveryTest < ActiveSupport::TestCase setup do @previous_logger = ActiveJob::Base.logger @previous_delivery_method = ActionMailer::Base.delivery_method + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + ActionMailer::Base.deliver_later_queue_name = :test_queue ActionMailer::Base.delivery_method = :test ActiveJob::Base.logger = Logger.new(nil) @mail = DelayedMailer.test_message(1, 2, 3) @@ -20,6 +22,7 @@ class MessageDeliveryTest < ActiveSupport::TestCase teardown do ActiveJob::Base.logger = @previous_logger ActionMailer::Base.delivery_method = @previous_delivery_method + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name end test 'should have a message' do @@ -80,4 +83,15 @@ class MessageDeliveryTest < ActiveSupport::TestCase end end + test 'should enqueue the job on the correct queue' do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "test_queue") do + @mail.deliver_later + end + end + + test 'can override the queue when enqueuing mail' do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "another_queue") do + @mail.deliver_later(queue: :another_queue) + end + end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 4e26a2a60e..8eea4ccd41 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,78 @@ +* Using strings or symbols for middleware class names is deprecated. Convert + things like this: + + middleware.use "Foo::Bar" + + to this: + + middleware.use Foo::Bar + +* ActionController::TestSession now accepts a default value as well as + a block for generating a default value based off the key provided. + + This fixes calls to session#fetch in ApplicationController instances that + take more two arguments or a block from raising `ArgumentError: wrong + number of arguments (2 for 1)` when performing controller tests. + + *Matthew Gerrior* + +* Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by + default block. + + *Jonas Schuber Erlandsson*, *Roque Pinel* + +* `ActionController::Parameters` no longer inherits from + `HashWithIndifferentAccess` + + Inheriting from `HashWithIndifferentAccess` allowed users to call any + enumerable methods on `Parameters` object, resulting in a risk of losing the + `permitted?` status or even getting back a pure `Hash` object instead of + a `Parameters` object with proper sanitization. + + By not inheriting from `HashWithIndifferentAccess`, we are able to make + sure that all methods that are defined in `Parameters` object will return + a proper `Parameters` object with a correct `permitted?` flag. + + *Prem Sichanugrist* + +* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch` + from the concurrent-ruby gem. + + *Jerry D'Antonio* + +* Add ability to filter parameters based on parent keys. + + # matches {credit_card: {code: "xxxx"}} + # doesn't match {file: { code: "xxxx"}} + config.filter_parameters += [ "credit_card.code" ] + + See #13897. + + *Guillaume Malette* + +* Deprecate passing first parameter as `Hash` and default status code for `head` method. + + *Mehmet Emin İNAÇ* + +* Adds`Rack::Utils::ParameterTypeError` and `Rack::Utils::InvalidParameterError` + to the rescue_responses hash in `ExceptionWrapper` (Rack recommends + integrators serve 400s for both of these). + + *Grey Baker* + +* Add support for API only apps. + ActionController::API is added as a replacement of + ActionController::Base for this kind of applications. + + *Santiago Pastorino & Jorge Bejar* + +* Remove `assigns` and `assert_template`. Both methods have been extracted + into a gem at https://github.com/rails/rails-controller-testing. + + See #18950. + + *Alan Guo Xiang Tan* + * `FileHandler` and `Static` middleware initializers accept `index` argument to configure the directory index file name. Defaults to `index` (as in `index.html`). @@ -62,6 +137,16 @@ *Adam Forsyth* +* Allow `Bearer` as token-keyword in `Authorization-Header`. + + Aditionally to `Token`, the keyword `Bearer` is acceptable as a keyword + for the auth-token. The `Bearer` keyword is described in the original + OAuth RFC and used in libraries like Angular-JWT. + + See #19094. + + *Peter Schröder* + * Drop request class from RouteSet constructor. If you would like to use a custom request class, please subclass and implement @@ -121,7 +206,8 @@ *arthurnn* * `ActionController#translate` supports symbols as shortcuts. - When shortcut is given it also lookups without action name. + When a shortcut is given it also performs the lookup without the action + name. *Max Melentiev* @@ -189,13 +275,13 @@ * Preserve default url options when generating URLs. - Fixes an issue that would cause default_url_options to be lost when + Fixes an issue that would cause `default_url_options` to be lost when generating URLs with fewer positional arguments than parameters in the route definition. *Tekin Suleyman* -* Deprecate *_via_redirect integration test methods. +* Deprecate `*_via_redirect` integration test methods. Use `follow_redirect!` manually after the request call for the same behavior. @@ -218,11 +304,11 @@ *Jonas Baumann* -* Deprecate all *_filter callbacks in favor of *_action callbacks. +* Deprecate all `*_filter` callbacks in favor of `*_action` callbacks. *Rafael Mendonça França* -* Allow you to pass `prepend: false` to protect_from_forgery to have the +* Allow you to pass `prepend: false` to `protect_from_forgery` to have the verification callback appended instead of prepended to the chain. This allows you to let the verification step depend on prior callbacks. diff --git a/actionpack/bin/test b/actionpack/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionpack/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +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 c95b9a4097..784092867c 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -88,7 +88,7 @@ module AbstractController # Returns the full controller name, underscored, without the ending Controller. # # class MyApp::MyPostsController < AbstractController::Base - # end + # # end # # MyApp::MyPostsController.controller_path # => "my_app/my_posts" @@ -96,7 +96,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. @@ -148,9 +148,6 @@ module AbstractController # # ==== Parameters # * <tt>action_name</tt> - The name of an action to be tested - # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) _find_action_name(action_name).present? end @@ -171,9 +168,6 @@ module AbstractController # ==== Parameters # * <tt>name</tt> - The name of an action to be tested # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> - # # :api: private def action_method?(name) self.class.action_methods.include?(name) diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 109eff10eb..d84c238a62 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -181,7 +181,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, '') + module_name = name.sub(/Controller$/, ''.freeze) module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 5514213ad8..887196b3d2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -54,11 +54,11 @@ module AbstractController Mime::TEXT end - DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( + DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i( @_action_name @_response_body @_formats @_prefixes @_config @_view_context_class @_view_renderer @_lookup_context @_routes @_db_runtime - ).map(&:to_sym) + ) # This method should return a hash with assigns. # You can overwrite this configuration per controller. diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index a1893ce920..89fc4520d3 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -7,6 +7,7 @@ require 'action_controller/metal/strong_parameters' module ActionController extend ActiveSupport::Autoload + autoload :API autoload :Base autoload :Caching autoload :Metal @@ -15,7 +16,6 @@ module ActionController autoload :FormBuilder autoload_under "metal" do - autoload :Compatibility autoload :ConditionalGet autoload :Cookies autoload :DataStreaming @@ -25,6 +25,7 @@ module ActionController autoload :Head autoload :Helpers autoload :HttpAuthentication + autoload :BasicImplicitRender autoload :ImplicitRender autoload :Instrumentation autoload :MimeResponds diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb new file mode 100644 index 0000000000..b4594bf302 --- /dev/null +++ b/actionpack/lib/action_controller/api.rb @@ -0,0 +1,147 @@ +require 'action_view' +require 'action_controller' +require 'action_controller/log_subscriber' + +module ActionController + # API Controller is a lightweight version of <tt>ActionController::Base</tt>, + # created for applications that don't require all functionalities that a complete + # \Rails controller provides, allowing you to create controllers with just the + # features that you need for API only applications. + # + # An API Controller is different from a normal controller in the sense that + # by default it doesn't include a number of features that are usually required + # by browser access only: layouts and templates rendering, cookies, sessions, + # flash, assets, and so on. This makes the entire controller stack thinner, + # suitable for API applications. It doesn't mean you won't have such + # features if you need them: they're all available for you to include in + # your application, they're just not part of the default API Controller stack. + # + # By default, only the ApplicationController in a \Rails application inherits + # from <tt>ActionController::API</tt>. All other controllers in turn inherit + # from ApplicationController. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # render json: @posts + # end + # end + # + # Request, response and parameters objects all work the exact same way as + # <tt>ActionController::Base</tt>. + # + # == Renders + # + # The default API Controller stack includes all renderers, which means you + # can use <tt>render :json</tt> and brothers freely in your controllers. Keep + # in mind that templates are not going to be rendered, so you need to ensure + # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in + # all actions, otherwise it will return 204 No Content response. + # + # def show + # @post = Post.find(params[:id]) + # render json: @post + # end + # + # == Redirects + # + # Redirects are used to move from one action to another. You can use the + # <tt>redirect</tt> method in your controllers in the same way as + # <tt>ActionController::Base</tt>. For example: + # + # def create + # redirect_to root_url and return if not_authorized? + # # do stuff here + # end + # + # == Adding new behavior + # + # In some scenarios you may want to add back some functionality provided by + # <tt>ActionController::Base</tt> that is not present by default in + # <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This + # module gives you the <tt>respond_to</tt> method. Adding it is quite simple, + # you just need to include the module in a specific controller or in + # +ApplicationController+ in case you want it available in your entire + # application: + # + # class ApplicationController < ActionController::API + # include ActionController::MimeResponds + # end + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # + # respond_to do |format| + # format.json { render json: @posts } + # format.xml { render xml: @posts } + # end + # end + # end + # + # Quite straightforward. Make sure to check <tt>ActionController::Base</tt> + # available modules if you want to include any other functionality that is + # not provided by <tt>ActionController::API</tt> out of the box. + class API < Metal + abstract! + + # Shortcut helper that returns all the ActionController::API modules except + # the ones passed as arguments: + # + # class MyAPIBaseController < ActionController::Metal + # ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left| + # include left + # end + # end + # + # This gives better control over what you want to exclude and makes it easier + # to create an API controller class, instead of listing the modules required + # manually. + def self.without_modules(*modules) + modules = modules.map do |m| + m.is_a?(Symbol) ? ActionController.const_get(m) : m + end + + MODULES - modules + end + + MODULES = [ + AbstractController::Rendering, + + UrlFor, + Redirecting, + Rendering, + Renderers::All, + ConditionalGet, + RackDelegation, + BasicImplicitRender, + StrongParameters, + + ForceSSL, + DataStreaming, + + # Before callbacks should also be executed as early as possible, so + # also include them at the bottom. + AbstractController::Callbacks, + + # Append rescue at the bottom to wrap as much as possible. + Rescue, + + # Add instrumentations hooks at the bottom, to ensure they instrument + # all the methods properly. + Instrumentation, + + # Params wrapper should come before instrumentation so they are + # properly showed in logs + ParamsWrapper + ] + + MODULES.each do |mod| + include mod + end + + 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 bfae372f53..55734b9774 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -50,9 +50,9 @@ module ActionController # # == Parameters # - # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method - # which returns a hash. For example, an action that was performed through <tt>/posts?category=All&limit=5</tt> will include - # <tt>{ "category" => "All", "limit" => "5" }</tt> in params. + # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are + # available through the params method which returns a hash. For example, an action that was performed through + # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in params. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # @@ -183,7 +183,7 @@ module ActionController # Shortcut helper that returns all the modules included in # ActionController::Base except the ones passed as arguments: # - # class MetalController + # class MyBaseController < ActionController::Metal # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| # include left # end @@ -252,7 +252,7 @@ module ActionController # Define some internal variables that should not be propagated to the view. PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ - :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, + :@_status, :@_headers, :@_params, :@_response, :@_request, :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] def _protected_ivars # :nodoc: diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index de85e0c1a7..a4e4992cfe 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -8,7 +8,7 @@ module ActionController # # You can read more about each approach by clicking the modules below. # - # Note: To turn off all caching, set + # Note: To turn off all caching provided by Action Controller, set # config.action_controller.perform_caching = false # # == \Caching stores diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 87609d8aa7..4c9f14e409 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionController status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? + message << " (#{additions.join(" | ".freeze)})" unless additions.blank? message end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index ae111e4951..914b0d4b30 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' +require 'active_support/deprecation' module ActionController # Extend ActionDispatch middleware stack to make it aware of options @@ -11,22 +12,14 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(klass, *args, &block) - options = args.extract_options! - @only = Array(options.delete(:only)).map(&:to_s) - @except = Array(options.delete(:except)).map(&:to_s) - args << options unless options.empty? - super + def initialize(klass, args, actions, strategy, block) + @actions = actions + @strategy = strategy + super(klass, args, block) end def valid?(action) - if @only.present? - @only.include?(action) - elsif @except.present? - !@except.include?(action) - else - true - end + @strategy.call @actions, action end end @@ -37,6 +30,32 @@ module ActionController middleware.valid?(action) ? middleware.build(a) : a end end + + private + + INCLUDE = ->(list, action) { list.include? action } + EXCLUDE = ->(list, action) { !list.include? action } + NULL = ->(list, action) { true } + + def build_middleware(klass, args, block) + options = args.extract_options! + only = Array(options.delete(:only)).map(&:to_s) + except = Array(options.delete(:except)).map(&:to_s) + args << options unless options.empty? + + strategy = NULL + list = nil + + if only.any? + strategy = INCLUDE + list = only + elsif except.any? + strategy = EXCLUDE + list = except + end + + Middleware.new(get_class(klass), args, list, strategy, block) + end end # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -98,11 +117,10 @@ module ActionController class Metal < AbstractController::Base abstract! - attr_internal_writer :env - def env - @_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>. @@ -197,8 +215,7 @@ module ActionController def set_request!(request) #:nodoc: @_request = request - @_env = request.env - @_env['action_controller.instance'] = self + @_request.controller_instance = self end def to_a #:nodoc: @@ -232,13 +249,13 @@ module ActionController end # Returns a Rack endpoint for the given action name. - def self.action(name, klass = ActionDispatch::Request) + def self.action(name) if middleware_stack.any? middleware_stack.build(name) do |env| - new.dispatch(name, klass.new(env)) + new.dispatch(name, ActionDispatch::Request.new(env)) end else - lambda { |env| new.dispatch(name, klass.new(env)) } + lambda { |env| new.dispatch(name, ActionDispatch::Request.new(env)) } end end end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb new file mode 100644 index 0000000000..6c6f8381ff --- /dev/null +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -0,0 +1,11 @@ +module ActionController + module BasicImplicitRender + def send_action(method, *args) + super.tap { default_render unless performed? } + end + + def default_render(*args) + head :no_content + end + end +end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 1abd8d3a33..e6d7f958bb 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -85,6 +85,10 @@ module ActionController #:nodoc: @to_path = path end + def body + File.binread(to_path) + end + # Stream the file's contents if Rack::Sendfile isn't present. def each File.open(to_path, 'rb') do |file| @@ -126,7 +130,7 @@ module ActionController #:nodoc: # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: send_file_headers! options - render options.slice(:status, :content_type).merge(:text => data) + render options.slice(:status, :content_type).merge(body: data) end private diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index d920668184..e31d65aac2 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -55,10 +55,10 @@ module ActionController # You can pass any of the following options to affect the before_action callback # * <tt>only</tt> - The callback should be run only for this action # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # * <tt>if</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a false value. def force_ssl(options = {}) action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ 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 & redirect options - # available to the <tt>force_ssl</tt> method. + # * <tt>host_or_options</tt> - Either a host name or any of the url & + # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 70f42bf565..f445094bdc 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -17,8 +17,18 @@ module ActionController # # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) - options, status = status, nil if status.is_a?(Hash) - status ||= options.delete(:status) || :ok + 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 + end + + status ||= :ok + location = options.delete(:location) content_type = options.delete(:content_type) diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b4da381d26..fcaf3e6425 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -73,7 +73,7 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= begin + @helper_proxy ||= begin proxy = ActionView::Base.new proxy.config = config.inheritable_copy proxy.extend(_helpers) @@ -100,7 +100,7 @@ module ActionController def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 32c3c9652f..bbb38cf8fc 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -94,7 +94,7 @@ module ActionController end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request) == 'Basic') + request.authorization.present? && (auth_scheme(request).downcase == 'basic') end def user_name_and_password(request) @@ -397,7 +397,7 @@ module ActionController # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token TOKEN_KEY = 'token=' - TOKEN_REGEX = /^Token / + TOKEN_REGEX = /^(Token|Bearer) / AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self @@ -493,7 +493,7 @@ module ActionController "Token #{values * ", "}" end - # Sets a WWW-Authenticate to let the client know a token is desired. + # Sets a WWW-Authenticate header to let the client know a token is desired. # # controller - ActionController::Base instance for the outgoing response. # realm - String realm to use in the header. @@ -502,7 +502,7 @@ module ActionController def authentication_request(controller, realm, message = nil) message ||= "HTTP Token: Access denied.\n" controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") - controller.__send__ :render, :text => message, :status => :unauthorized + controller.__send__ :render, plain: message, status: :unauthorized end end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 1573ea7099..17fcc2fa02 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,17 +1,29 @@ module ActionController module ImplicitRender - def send_action(method, *args) - ret = super - default_render unless performed? - ret - end + include BasicImplicitRender + + # Renders the template corresponding to the controller action, if it exists. + # The action name, format, and variant are all taken into account. + # For example, the "new" action with an HTML format and variant "phone" + # would try to render the <tt>new.html+phone.erb</tt> template. + # + # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless + # a block is passed. In that case, it will override the super implementation. + # + # default_render do + # head 404 # No template was found + # end def default_render(*args) if template_exists?(action_name.to_s, _prefixes, variants: request.variant) render(*args) else - logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger - head :no_content + if block_given? + yield(*args) + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super + end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index fab1be3459..1db68db20f 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -156,16 +156,16 @@ module ActionController #:nodoc: # It works for both inline: # # respond_to do |format| - # format.html.any { render text: "any" } - # format.html.phone { render text: "phone" } + # format.html.any { render html: "any" } + # format.html.phone { render html: "phone" } # end # # and block syntax: # # respond_to do |format| # format.html do |variant| - # variant.any(:tablet, :phablet){ render text: "any" } - # variant.phone { render text: "phone" } + # variant.any(:tablet, :phablet){ render html: "any" } + # variant.phone { render html: "phone" } # end # end # diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 8a4ea70649..e680432127 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -4,8 +4,8 @@ require 'active_support/core_ext/module/anonymous' require 'action_dispatch/http/mime_type' module ActionController - # Wraps the parameters hash into a nested hash. This will allow clients to submit - # POST requests without having to specify any root elements. + # Wraps the parameters hash into a nested hash. This will allow clients to + # submit requests without having to specify any root elements. # # This functionality is enabled in +config/initializers/wrap_parameters.rb+ # and can be customized. @@ -14,7 +14,7 @@ module ActionController # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters format: [:json, :xml] + # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -40,7 +40,7 @@ module ActionController # wrap_parameters :person, include: [:username, :password] # end # - # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # On Active Record models with no +:include+ or +:exclude+ option set, # it will only wrap the parameters returned by the class method # <tt>attribute_names</tt>. # diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 45d3962494..cb74c4f0d4 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -94,7 +94,7 @@ module ActionController # This method is the opposite of add method. # - # Usage: + # To remove a csv renderer: # # ActionController::Renderers.remove(:csv) def self.remove(key) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index b9ae8dd5ea..a3b0645dc0 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,3 +1,6 @@ +require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' + module ActionController module Rendering extend ActiveSupport::Concern @@ -74,6 +77,17 @@ module ActionController def _normalize_options(options) #:nodoc: _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/plain`. + WARNING + end + if options[:html] options[:html] = ERB::Util.html_escape(options[:html]) end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index a7bd3622ee..d21a778d8d 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -20,7 +20,7 @@ module ActionController #:nodoc: # Since HTML and JavaScript requests are typically made from the browser, we # need to ensure to verify request authenticity for the web browser. We can # use session-oriented authentication for these types of requests, by using - # the `protect_form_forgery` method in our controllers. + # the `protect_from_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -139,7 +139,7 @@ module ActionController #:nodoc: request.session = NullSessionHash.new(request.env) request.env['action_dispatch.request.flash_hash'] = nil request.env['rack.session.options'] = { skip: true } - request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + request.cookie_jar = NullCookieJar.build(request, {}) end protected @@ -160,14 +160,6 @@ module ActionController #:nodoc: end class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def self.build(request) - key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options_for_env({})) - end - def write(*) # nothing end @@ -258,7 +250,7 @@ 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? diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index af31de1f3a..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -199,7 +199,7 @@ module ActionController #:nodoc: def _process_options(options) #:nodoc: super if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" + if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index c98e937423..fc8e345d43 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -11,9 +11,9 @@ module ActionController # # params = ActionController::Parameters.new(a: {}) # params.fetch(:b) - # # => ActionController::ParameterMissing: param not found: b + # # => ActionController::ParameterMissing: param is missing or the value is empty: b # params.require(:a) - # # => ActionController::ParameterMissing: param not found: a + # # => ActionController::ParameterMissing: param is missing or the value is empty: a class ParameterMissing < KeyError attr_reader :param # :nodoc: @@ -23,11 +23,13 @@ module ActionController end end - # Raised when a supplied parameter is not expected. + # Raised when a supplied parameter is not expected and + # ActionController::Parameters.action_on_unpermitted_parameters + # is set to <tt>:raise</tt>. # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b class UnpermittedParameters < IndexError attr_reader :params # :nodoc: @@ -102,10 +104,12 @@ module ActionController # params = ActionController::Parameters.new(key: 'value') # params[:key] # => "value" # params["key"] # => "value" - class Parameters < ActiveSupport::HashWithIndifferentAccess + class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' # because they are added by Rails and should be of no concern. One way @@ -142,11 +146,22 @@ module ActionController # params = ActionController::Parameters.new(name: 'Francesco') # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> - def initialize(attributes = nil) - super(attributes) + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access @permitted = self.class.permit_all_parameters end + # Returns true if another +Parameters+ object contains the same content and + # permitted flag, or other Hash-like object contains the same content. This + # override is in place so you can perform a comparison with `Hash`. + def ==(other_hash) + if other_hash.respond_to?(:permitted?) + super + else + @parameters == other_hash + end + end + # Returns a safe +Hash+ representation of this parameter with all # unpermitted keys removed. # @@ -160,7 +175,7 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - to_hash + @parameters.to_h else slice(*self.class.always_permitted_parameters).permit!.to_h end @@ -168,20 +183,17 @@ module ActionController # Returns an unsafe, unfiltered +Hash+ representation of this parameter. def to_unsafe_h - to_hash + @parameters.to_h end alias_method :to_unsafe_hash, :to_unsafe_h # Convert all hashes in values into parameters, then yield each pair like # the same way as <tt>Hash#each_pair</tt> def each_pair(&block) - super do |key, value| - convert_hashes_to_parameters(key, value) + @parameters.each_pair do |key, value| + yield key, convert_hashes_to_parameters(key, value) end - - super end - alias_method :each, :each_pair # Attribute that keeps track of converted arrays, if any, to avoid double @@ -236,10 +248,10 @@ module ActionController # # => {"name"=>"Francesco"} # # ActionController::Parameters.new(person: nil).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # => ActionController::ParameterMissing: param is missing or the value is empty: person # # ActionController::Parameters.new(person: {}).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # => ActionController::ParameterMissing: param is missing or the value is empty: person def require(key) value = self[key] if value.present? || value == false @@ -345,7 +357,13 @@ module ActionController # params[:person] # => {"name"=>"Francesco"} # params[:none] # => nil def [](key) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, @parameters[key]) + end + + # Assigns a value to a given +key+. The given key may still get filtered out + # when +permit+ is called. + def []=(key, value) + @parameters[key] = value end # Returns a parameter for the given +key+. If the +key+ @@ -356,13 +374,19 @@ module ActionController # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) # params.fetch(:person) # => {"name"=>"Francesco"} - # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none + # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" - def fetch(key, *args) - convert_hashes_to_parameters(key, super, false) - rescue KeyError - raise ActionController::ParameterMissing.new(key) + def fetch(key, *args, &block) + convert_value_to_parameters( + @parameters.fetch(key) { + if block_given? + yield + else + args.fetch(0) { raise ActionController::ParameterMissing.new(key) } + end + } + ) end # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -373,7 +397,24 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) + end + + # Returns current <tt>ActionController::Parameters</tt> instance which + # contains only the given +keys+. + def slice!(*keys) + @parameters.slice!(*keys) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # filters out the given +keys+. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.except(:a, :b) # => {"c"=>3} + # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3} + def except(*keys) + new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end # Removes and returns the key/value pairs matching the given keys. @@ -382,7 +423,7 @@ module ActionController # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} # params # => {"c"=>3} def extract!(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end # Returns a new <tt>ActionController::Parameters</tt> with the results of @@ -391,36 +432,80 @@ module ActionController # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } # # => {"a"=>2, "b"=>4, "c"=>6} - def transform_values - if block_given? - new_instance_with_inherited_permitted_status(super) + def transform_values(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_values(&block) + ) else - super + @parameters.transform_values end end - # This method is here only to make sure that the returned object has the - # correct +permitted+ status. It should not matter since the parent of - # this object is +HashWithIndifferentAccess+ - def transform_keys # :nodoc: - if block_given? - new_instance_with_inherited_permitted_status(super) + # Performs values transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_values!(&block) + @parameters.transform_values!(&block) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance with the + # results of running +block+ once for every key. The values are unchanged. + def transform_keys(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_keys(&block) + ) else - super + @parameters.transform_keys end end + # Performs keys transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_keys!(&block) + @parameters.transform_keys!(&block) + self + end + # Deletes and returns a key-value pair from +Parameters+ whose key is equal # 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, &block) - convert_hashes_to_parameters(key, super, false) + convert_value_to_parameters(@parameters.delete(key)) + end + + # Returns a new instance of <tt>ActionController::Parameters</tt> with only + # items that the block evaluates to true. + def select(&block) + new_instance_with_inherited_permitted_status(@parameters.select(&block)) end # Equivalent to Hash#keep_if, but returns nil if no changes were made. def select!(&block) - convert_value_to_parameters(super) + @parameters.select!(&block) + self + end + alias_method :keep_if, :select! + + # Returns a new instance of <tt>ActionController::Parameters</tt> with items + # that the block evaluates to true removed. + def reject(&block) + new_instance_with_inherited_permitted_status(@parameters.reject(&block)) + end + + # Removes items that the block evaluates to true and returns self. + def reject!(&block) + @parameters.reject!(&block) + self + end + alias_method :delete_if, :reject! + + # Return values that were assigned to the given +keys+. Note that all the + # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>. + def values_at(*keys) + convert_value_to_parameters(@parameters.values_at(*keys)) end # Returns an exact copy of the <tt>ActionController::Parameters</tt> @@ -437,11 +522,30 @@ module ActionController end end + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # +other_hash+ merges into current hash. + def merge(other_hash) + new_instance_with_inherited_permitted_status( + @parameters.merge(other_hash) + ) + end + + # 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. + def stringify_keys # :nodoc: + dup + end + protected def permitted=(new_permitted) @permitted = new_permitted end + def fields_for_style? + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + private def new_instance_with_inherited_permitted_status(hash) self.class.new(hash).tap do |new_instance| @@ -449,40 +553,41 @@ module ActionController end end - def convert_hashes_to_parameters(key, value, assign_if_converted=true) + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) - self[key] = converted if assign_if_converted && !converted.equal?(value) + @parameters[key] = converted unless converted.equal?(value) converted end def convert_value_to_parameters(value) - if value.is_a?(Array) && !converted_arrays.member?(value) + case value + when Array + return value if converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted - elsif value.is_a?(Parameters) || !value.is_a?(Hash) - value - else + when Hash self.class.new(value) + else + value end end def each_element(object) - if object.is_a?(Array) - object.map { |el| yield el }.compact - elsif fields_for_style?(object) - hash = object.class.new - object.each { |k,v| hash[k] = yield v } - hash - else - yield object + case object + when Array + object.grep(Parameters).map { |el| yield el }.compact + when Parameters + if object.fields_for_style? + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + yield object + end end end - def fields_for_style?(object) - object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } - end - def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -544,14 +649,8 @@ module ActionController end def array_of_permitted_scalars?(value) - if value.is_a?(Array) - value.all? {|element| permitted_scalar?(element)} - end - end - - def array_of_permitted_scalars_filter(params, key) - if has_key?(key) && array_of_permitted_scalars?(self[key]) - params[key] = self[key] + if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} + yield value end end @@ -562,17 +661,17 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| next unless value + next unless has_key? key if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. - array_of_permitted_scalars_filter(params, key) + array_of_permitted_scalars?(self[key]) do |val| + params[key] = val + end else # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| - if element.is_a?(Hash) - element = self.class.new(element) unless element.respond_to?(:permit) - element.permit(*Array.wrap(filter[key])) - end + element.permit(*Array.wrap(filter[key])) end end end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 5a0e5c62e4..dbf7241a14 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -41,7 +41,11 @@ module ActionController if original_script_name options[:original_script_name] = original_script_name else - options[:script_name] = same_origin ? request.script_name.dup : script_name + if same_origin + options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup + else + options[:script_name] = script_name + end end options.freeze else diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb deleted file mode 100644 index 437fec3dc6..0000000000 --- a/actionpack/lib/action_controller/middleware.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController - class Middleware < Metal - class ActionMiddleware - def initialize(controller, app) - @controller, @app = controller, app - end - - def call(env) - request = ActionDispatch::Request.new(env) - @controller.build(@app).dispatch(:index, request) - end - end - - class << self - alias build new - - def new(app) - ActionMiddleware.new(self, app) - end - end - - attr_internal :app - - def process(action) - response = super - self.status, self.headers, self.response_body = response if response.is_a?(Array) - response - end - - def initialize(app) - super() - @_app = app - end - - def index - call(env) - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb index 304012d24d..0179f4afcd 100644 --- a/actionpack/lib/action_controller/template_assertions.rb +++ b/actionpack/lib/action_controller/template_assertions.rb @@ -1,188 +1,9 @@ module ActionController module TemplateAssertions - extend ActiveSupport::Concern - - included do - setup :setup_subscriptions - teardown :teardown_subscriptions - end - - RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze - - def setup_subscriptions - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - instance_variable_set("@_#{instance_variable}", Hash.new(0)) - end - - @_subscribers = [] - - @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:layout] - if path - @_layouts[path] += 1 - if path =~ /^layouts\/(.*)/ - @_layouts[$1] += 1 - end - end - end - - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - if virtual_path = payload[:virtual_path] - partial = virtual_path =~ /^.*\/_[^\/]*$/ - - if partial - @_partials[virtual_path] += 1 - @_partials[virtual_path.split("/").last] += 1 - end - - @_templates[virtual_path] += 1 - else - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 - end - end - end - end - - def teardown_subscriptions - @_subscribers.each do |subscriber| - ActiveSupport::Notifications.unsubscribe(subscriber) - end - end - - def process(*args) - reset_template_assertion - super - end - - def reset_template_assertion - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - ivar_name = "@_#{instance_variable}" - if instance_variable_defined?(ivar_name) - instance_variable_get(ivar_name).clear - end - end - end - - # Asserts that the request was rendered with the appropriate template file or partials. - # - # # assert that the "new" view template was rendered - # assert_template "new" - # - # # assert that the exact template "admin/posts/new" was rendered - # assert_template %r{\Aadmin/posts/new\Z} - # - # # assert that the layout 'admin' was rendered - # assert_template layout: 'admin' - # assert_template layout: 'layouts/admin' - # assert_template layout: :admin - # - # # assert that no layout was rendered - # assert_template layout: nil - # assert_template layout: false - # - # # assert that the "_customer" partial was rendered twice - # assert_template partial: '_customer', count: 2 - # - # # assert that no partials were rendered - # assert_template partial: false - # - # # assert that a file was rendered - # assert_template file: "README.rdoc" - # - # # assert that no file was rendered - # assert_template file: nil - # assert_template file: false - # - # In a view test case, you can also assert that specific locals are passed - # to partials: - # - # # assert that the "_customer" partial was rendered with a specific object - # assert_template partial: '_customer', locals: { customer: @customer } def assert_template(options = {}, message = nil) - # Force body to be read in case the template is being streamed. - response.body - - case options - when NilClass, Regexp, String, Symbol - options = options.to_s if Symbol === options - rendered = @_templates - msg = message || sprintf("expecting <%s> but rendering with <%s>", - options.inspect, rendered.keys) - matches_template = - case options - when String - !options.empty? && rendered.any? do |t, num| - options_splited = options.split(File::SEPARATOR) - t_splited = t.split(File::SEPARATOR) - t_splited.last(options_splited.size) == options_splited - end - when Regexp - rendered.any? { |t,num| t.match(options) } - when NilClass - rendered.blank? - end - assert matches_template, msg - when Hash - options.assert_valid_keys(:layout, :partial, :locals, :count, :file) - - if options.key?(:layout) - expected_layout = options[:layout] - msg = message || sprintf("expecting layout <%s> but action rendered <%s>", - expected_layout, @_layouts.keys) - - case expected_layout - when String, Symbol - assert_includes @_layouts.keys, expected_layout.to_s, msg - when Regexp - assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil, false - assert(@_layouts.empty?, msg) - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Regexp, nil or false for :layout" - end - end - - if options[:file] - assert_includes @_files.keys, options[:file] - elsif options.key?(:file) - assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" - end - - if expected_partial = options[:partial] - if expected_locals = options[:locals] - if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') - - partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view - assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg - - msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, - expected_locals, - @_rendered_views.locals_for(view)] - assert(@_rendered_views.view_rendered?(view, options[:locals]), msg) - else - warn "the :locals option to #assert_template is only supported in a ActionView::TestCase" - end - elsif expected_count = options[:count] - actual_count = @_partials[expected_partial] - msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)", - expected_partial, expected_count, actual_count) - assert(actual_count == expected_count.to_i, msg) - else - msg = message || sprintf("expecting partial <%s> but action rendered <%s>", - options[:partial], @_partials.keys) - assert_includes @_partials, expected_partial, msg - end - elsif options.key?(:partial) - assert @_partials.empty?, - "Expected no partials to be rendered" - end - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil" - end + raise NoMethodError, + "assert_template has been extracted to a gem. To continue using it, + add `gem 'rails-controller-testing'` to your Gemfile." end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b29c5b23fc..e012fa617e 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,4 +1,5 @@ require 'rack/session/abstract/id' +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' @@ -10,28 +11,44 @@ module ActionController DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' - def initialize(env = {}) - super + def self.new_session + TestSession.new + end + + # 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 + new(default_env.merge(env), new_session) + end - self.session = TestSession.new + def self.default_env + DEFAULT_ENV + end + private_class_method :default_env + + def initialize(env, session) + super(env) + + self.session = session self.session_options = TestSession::DEFAULT_OPTIONS end - def assign_parameters(routes, controller_path, action, parameters = {}) - parameters = parameters.symbolize_keys - extra_keys = routes.extra_keys(parameters.merge(:controller => controller_path, :action => action)) - non_path_parameters = get? ? query_parameters : request_parameters + def query_string=(string) + @env[Rack::QUERY_STRING] = string + end - parameters.each do |key, value| - if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) - value = value.map{ |v| v.duplicable? ? v.dup : v } - elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) - value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] - elsif value.frozen? && value.duplicable? - value = value.dup - end + def request_parameters=(params) + @env["action_dispatch.request.request_parameters"] = params + end - if extra_keys.include?(key) || key == :action || key == :controller + def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) + non_path_parameters = {} + path_parameters = {} + + parameters.each do |key, value| + if query_string_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -44,69 +61,83 @@ module ActionController end end - path_parameters[:controller] = controller_path - path_parameters[:action] = action - - # Clear the combined params hash in case it was already referenced. - @env.delete("action_dispatch.request.parameters") + if get? + if self.query_string.blank? + self.query_string = non_path_parameters.to_query + end + else + if ENCODER.should_multipart?(non_path_parameters) + @env['CONTENT_TYPE'] = ENCODER.content_type + data = ENCODER.build_multipart non_path_parameters + else + @env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded' + + # FIXME: setting `request_parametes` is normally handled by the + # params parser middleware, and we should remove this roundtripping + # when we switch to caling `call` on the controller + + case content_mime_type.ref + when :json + data = ActiveSupport::JSON.encode(non_path_parameters) + params = ActiveSupport::JSON.decode(data).with_indifferent_access + self.request_parameters = params + when :xml + data = non_path_parameters.to_xml + params = Hash.from_xml(data)['hash'] + self.request_parameters = params + when :url_encoded_form + data = non_path_parameters.to_query + else + raise "Unknown Content-Type: #{content_type}" + end + end - # Clear the filter cache variables so they're not stale - @filtered_parameters = @filtered_env = @filtered_path = nil + @env['CONTENT_LENGTH'] = data.length.to_s + @env['rack.input'] = StringIO.new(data) + end - data = request_parameters.to_query - @env['CONTENT_LENGTH'] = data.length.to_s - @env['rack.input'] = StringIO.new(data) - end + @env["PATH_INFO"] ||= generated_path + path_parameters[:controller] = controller_path + path_parameters[:action] = action - def recycle! - @formats = nil - @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } - @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @method = @request_method = nil - @fullpath = @ip = @remote_ip = @protocol = nil - @env['action_dispatch.request.query_parameters'] = {} - @set_cookies ||= {} - @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) - deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") - @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } - cookie_jar.update(rack_cookies) - cookie_jar.update(cookies) - cookie_jar.update(@set_cookies) - cookie_jar.recycle! + self.path_parameters = path_parameters end - private + ENCODER = Class.new do + include Rack::Test::Utils + + def should_multipart?(params) + # FIXME: lifted from Rack-Test. We should push this separation upstream + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when Rack::Test::UploadedFile + multipart = true + end + } + params.values.each(&query) + multipart + end - def default_env - DEFAULT_ENV - end - end + public :build_multipart - class TestResponse < ActionDispatch::TestResponse - def recycle! - initialize - end + def content_type + "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" + end + end.new end class LiveTestResponse < Live::Response - def recycle! - @body = nil - initialize - end - - def body - @body ||= super - end - # Was the response successful? alias_method :success?, :successful? # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? end @@ -139,6 +170,10 @@ module ActionController clear end + def fetch(*args, &block) + @data.fetch(*args, &block) + end + private def load! @@ -165,7 +200,7 @@ module ActionController # class BooksControllerTest < ActionController::TestCase # def test_create # # Simulate a POST response with the given HTTP parameters. - # post(:create, book: { title: "Love Hina" }) + # post(:create, params: { book: { title: "Love Hina" }}) # # # Assert that the controller tried to redirect us to # # the created book's URI. @@ -195,7 +230,7 @@ module ActionController # request. You can modify this object before sending the HTTP request. For example, # you might want to set some session properties before sending a GET request. # <b>@response</b>:: - # An ActionController::TestResponse object, representing the response + # An ActionDispatch::TestResponse object, representing the response # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. @@ -218,21 +253,15 @@ module ActionController # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions # can be used against. These collections are: # - # * assigns: Instance variables assigned in the action that are available for the view. # * session: Objects being saved in the session. # * flash: The flash objects currently in the session. # * cookies: \Cookies being sent to the user on this request. # # These collections can be used just like any other hash: # - # assert_not_nil assigns(:person) # makes sure that a @person instance variable was set # 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 # - # For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To - # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing. - # So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work. - # # 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 @@ -323,7 +352,9 @@ 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) - process_with_kwargs("GET", action, *args) + res = process_with_kwargs("GET", action, *args) + cookies.update res.cookies + res end # Simulate a POST request with the given parameters and set/volley the response. @@ -371,19 +402,6 @@ module ActionController end alias xhr :xml_http_request - def paramify_values(hash_or_array_or_value) - case hash_or_array_or_value - when Hash - Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] - when Array - hash_or_array_or_value.map {|i| paramify_values(i)} - when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile - hash_or_array_or_value - else - hash_or_array_or_value.to_param - end - end - # Simulate a HTTP request to +action+ by specifying request method, # parameters and set/volley the response. # @@ -443,10 +461,6 @@ module ActionController parameters ||= {} - # Ensure that numbers and symbols passed as params are converted to - # proper params, as is the case when engaging rack. - parameters = paramify_values(parameters) if html_format?(parameters) - if format.present? parameters[:format] = format end @@ -457,17 +471,24 @@ module ActionController @controller.extend(Testing::Functional) end - @request.recycle! - @response.recycle! + self.cookies.update @request.cookies + @request.env['HTTP_COOKIE'] = cookies.to_header + @request.env['action_dispatch.cookies'] = nil + + @request = TestRequest.new scrub_env!(@request.env), @request.session + @response = build_response @response_klass + @response.request = @request @controller.recycle! @request.env['REQUEST_METHOD'] = http_method - controller_class_name = @controller.class.anonymous? ? - "anonymous" : - @controller.class.controller_path + parameters = parameters.symbolize_keys - @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) + generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) + generated_path = generated_path(generated_extras) + query_string_keys = query_parameter_names(generated_extras) + + @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys) @request.session.update(session) if session @request.flash.update(flash || {}) @@ -480,20 +501,21 @@ module ActionController @controller.request = @request @controller.response = @response - build_request_uri(controller_class_name, action, parameters) + @request.env["SCRIPT_NAME"] ||= @controller.config.relative_url_root @controller.recycle! @controller.process(action) + @request.env.delete 'HTTP_COOKIE' + if cookies = @request.env['action_dispatch.cookies'] unless @response.committed? cookies.write(@response) + self.cookies.update(cookies.instance_variable_get(:@cookies)) end end @response.prepare! - @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} - if flash_value = @request.flash.to_session_value @request.session['flash'] = flash_value else @@ -504,18 +526,31 @@ module ActionController @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end + @request.query_string = '' @response end + def controller_class_name + @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path + end + + def generated_path(generated_extras) + generated_extras[0] + end + + def query_parameter_names(generated_extras) + generated_extras[1] + [:controller, :action] + end + def setup_controller_request_and_response @controller = nil unless defined? @controller - response_klass = TestResponse + @response_klass = ActionDispatch::TestResponse if klass = self.class.controller_class if klass < ActionController::Live - response_klass = LiveTestResponse + @response_klass = LiveTestResponse end unless @controller begin @@ -526,8 +561,8 @@ module ActionController end end - @request = build_request - @response = build_response response_klass + @request = TestRequest.create + @response = build_response @response_klass @response.request = @request if @controller @@ -536,10 +571,6 @@ module ActionController end end - def build_request - TestRequest.new - end - def build_response(klass) klass.new end @@ -553,12 +584,20 @@ module ActionController private + def scrub_env!(env) + env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + env.delete 'action_dispatch.request.query_parameters' + env.delete 'action_dispatch.request.request_parameters' + 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.present? + non_kwarg_request_warning if args.any? args = args.unshift(http_method) process(action, *args) @@ -599,23 +638,6 @@ module ActionController end end - def build_request_uri(controller_class_name, action, parameters) - unless @request.env["PATH_INFO"] - options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters - options.update( - :controller => controller_class_name, - :action => action, - :relative_url_root => nil, - :_recall => @request.path_parameters) - - url, query_string = @routes.path_for(options).split("?", 2) - - @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root - @request.env["PATH_INFO"] = url - @request.env["QUERY_STRING"] = query_string || "" - end - end - def html_format?(parameters) return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index dcd3ee0644..f6336c8c7a 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -52,6 +52,7 @@ module ActionDispatch autoload :DebugExceptions autoload :ExceptionWrapper autoload :Flash + autoload :LoadInterlock autoload :ParamsParser autoload :PublicExceptions autoload :Reloader diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 747d295261..cc1cb3f0f0 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -151,11 +151,11 @@ module ActionDispatch control.merge! @cache_control if control.empty? - headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL + self[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL elsif control[:no_cache] - headers[CACHE_CONTROL] = NO_CACHE + self[CACHE_CONTROL] = NO_CACHE if control[:extras] - headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}" + self[CACHE_CONTROL] += ", #{control[:extras].join(', ')}" end else extras = control[:extras] @@ -167,7 +167,7 @@ module ActionDispatch options << MUST_REVALIDATE if control[:must_revalidate] options.concat(extras) if extras - headers[CACHE_CONTROL] = options.join(", ") + self[CACHE_CONTROL] = options.join(", ") end end end diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index bf79963351..94c1f2b41f 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,8 +5,7 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location # :nodoc: - filters = location_filter - if !filters.empty? && location_filter_match?(filters) + if location_filter_match? FILTERED else location @@ -15,7 +14,7 @@ module ActionDispatch private - def location_filter + def location_filters if request request.env['action_dispatch.redirect_filter'] || [] else @@ -23,12 +22,12 @@ module ActionDispatch end end - def location_filter_match?(filters) - filters.any? do |filter| + def location_filter_match? + location_filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter - location.match(filter) + location =~ filter end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 7e585aa244..a639f8a8f8 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -45,7 +45,7 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } + # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.xml { render xml: @post } # end # end @@ -211,7 +211,7 @@ module Mime # This method is opposite of register method. # - # Usage: + # To unregister a MIME type: # # Mime::Type.unregister(:mobile) def unregister(symbol) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index df4b073a17..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -30,36 +30,46 @@ module ActionDispatch when Regexp regexps << item else - strings << item.to_s + strings << Regexp.escape(item.to_s) end end - regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - new regexps, blocks + deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } + deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } + + regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks end - attr_reader :regexps, :blocks + attr_reader :regexps, :deep_regexps, :blocks - def initialize(regexps, blocks) + def initialize(regexps, deep_regexps, blocks) @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil @blocks = blocks end - def call(original_params) + def call(original_params, parents = []) filtered_params = {} original_params.each do |key, value| + parents.push(key) if deep_regexps if regexps.any? { |r| key =~ r } value = FILTERED + elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } + value = FILTERED elsif value.is_a?(Hash) - value = call(value) + value = call(value, parents) elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v) : v } + value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } elsif blocks.any? key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end + parents.pop if deep_regexps filtered_params[key] = value end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c2f05ecc86..4defb7f858 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -37,22 +37,7 @@ module ActionDispatch # Convert nested Hash to HashWithIndifferentAccess. # def normalize_encode_params(params) - case params - when Hash - if params.has_key?(:tempfile) - UploadedFile.new(params) - else - params.each_with_object({}) do |(key, val), new_hash| - new_hash[key] = if val.is_a?(Array) - val.map! { |el| normalize_encode_params(el) } - else - normalize_encode_params(val) - end - end.with_indifferent_access - end - else - params - end + ActionDispatch::Request::Utils.normalize_encode_params params end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index fe83c01562..de28cd0998 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -34,12 +34,14 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA ].freeze + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP + HTTP_X_FORWARDED_FOR HTTP_VERSION + ].freeze ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset - @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + @env["#{env}".freeze] # @env["HTTP_ACCEPT_CHARSET".freeze] end # end METHOD end @@ -103,13 +105,17 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= check_method(env["REQUEST_METHOD"]) + @request_method ||= check_method(super) end def routes # :nodoc: env["action_dispatch.routes".freeze] end + def routes=(routes) # :nodoc: + env["action_dispatch.routes".freeze] = routes + end + def original_script_name # :nodoc: env['ORIGINAL_SCRIPT_NAME'.freeze] end @@ -118,12 +124,31 @@ module ActionDispatch env[_routes.env_key] end + def engine_script_name=(name) # :nodoc: + env[routes.env_key] = name.dup + end + def request_method=(request_method) #:nodoc: if check_method(request_method) @request_method = env["REQUEST_METHOD"] = request_method end end + def controller_instance # :nodoc: + env['action_controller.instance'.freeze] + end + + def controller_instance=(controller) # :nodoc: + env['action_controller.instance'.freeze] = controller + end + + 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 + # once. + !(env['action_dispatch.show_exceptions'.freeze] == false) + end + # Returns a symbol form of the #request_method def request_method_symbol HTTP_METHOD_LOOKUP[request_method] @@ -210,6 +235,10 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end + def remote_ip=(remote_ip) + @env["action_dispatch.remote_ip".freeze] = remote_ip + end + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: # Returns the unique request id, which is based on either the X-Request-Id header that can @@ -228,7 +257,7 @@ module ActionDispatch alias_method :uuid, :request_id - def x_request_id # :nodoc + def x_request_id # :nodoc: @env[HTTP_X_REQUEST_ID] end @@ -259,7 +288,7 @@ module ActionDispatch end end - # returns true if request content mime type is + # Returns true if the request's content MIME type is # +application/x-www-form-urlencoded+ or +multipart/form-data+. def form_data? FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s) @@ -290,7 +319,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + @env["action_dispatch.request.query_parameters"] ||= normalize_encode_params(super || {}) rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:query, e) end @@ -298,7 +327,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + @env["action_dispatch.request.request_parameters"] ||= normalize_encode_params(super || {}) rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end @@ -318,10 +347,13 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected - def parse_query(*) - Utils.deep_munge(super) - end + def request_parameters=(params) + env["action_dispatch.request.request_parameters".freeze] = params + end + + def logger + env["action_dispatch.logger".freeze] + end private def check_method(name) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 9e53a0f08b..fd92e89231 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -40,10 +40,9 @@ module ActionDispatch # :nodoc: attr_writer :sending_file - # Get and set headers for this response. - attr_accessor :header + # Get headers for this response. + attr_reader :header - alias_method :headers=, :header= alias_method :headers, :header delegate :[], :[]=, :to => :@header @@ -61,7 +60,7 @@ module ActionDispatch # :nodoc: # The charset of the response. HTML wants to know the encoding of the # content you're giving them, so we need to send that along. - attr_accessor :charset + attr_reader :charset CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze @@ -81,11 +80,21 @@ module ActionDispatch # :nodoc: @response = response @buf = buf @closed = false + @str_body = nil + end + + def body + @str_body ||= begin + buf = '' + each { |chunk| buf << chunk } + buf + end end def write(string) raise IOError, "closed stream" if closed? + @str_body = nil @response.commit! @buf.push string end @@ -117,8 +126,9 @@ module ActionDispatch # :nodoc: super() header = merge_default_headers(header, default_headers) + @header = header - self.body, self.header, self.status = body, header, status + self.body, self.status = body, status @sending_file = false @blank = false @@ -127,7 +137,7 @@ module ActionDispatch # :nodoc: @sending = false @sent = false @content_type = nil - @charset = nil + @charset = self.class.default_charset if content_type = self[CONTENT_TYPE] type, charset = content_type.split(/;\s*charset=/) @@ -187,6 +197,15 @@ module ActionDispatch # :nodoc: @content_type = content_type.to_s end + # Sets the HTTP character set. In case of nil parameter + # it sets the charset to utf-8. + # + # response.charset = 'utf-16' # => 'utf-16' + # response.charset = nil # => 'utf-8' + def charset=(charset) + @charset = charset.nil? ? self.class.default_charset : charset + end + # The response code of the request. def response_code @status @@ -213,9 +232,7 @@ module ActionDispatch # :nodoc: # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body - strings = [] - each { |part| strings << part.to_s } - strings.join + @stream.body end EMPTY = " " @@ -278,6 +295,7 @@ module ActionDispatch # :nodoc: # # status, headers, body = *response def to_a + commit! rack_response @status, @header.to_hash end alias prepare! to_a @@ -302,6 +320,9 @@ module ActionDispatch # :nodoc: private def before_committed + return if committed? + assign_default_content_type_and_charset! + handle_conditional_get! end def before_sending @@ -319,16 +340,15 @@ module ActionDispatch # :nodoc: body.respond_to?(:each) ? body : [body] end - def assign_default_content_type_and_charset!(headers) - return if headers[CONTENT_TYPE].present? + def assign_default_content_type_and_charset! + return if self[CONTENT_TYPE].present? @content_type ||= Mime::HTML - @charset ||= self.class.default_charset unless @charset == false type = @content_type.to_s.dup - type << "; charset=#{@charset}" if append_charset? + type << "; charset=#{charset}" if append_charset? - headers[CONTENT_TYPE] = type + self[CONTENT_TYPE] = type end def append_charset? @@ -372,9 +392,6 @@ module ActionDispatch # :nodoc: end def rack_response(status, header) - assign_default_content_type_and_charset!(header) - handle_conditional_get! - header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) if NO_CONTENT_CODES.include?(@status) diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 540e11a4a0..a221f4c5af 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -28,7 +28,13 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile @original_filename = hash[:filename] - @original_filename &&= @original_filename.encode "UTF-8" + if @original_filename + begin + @original_filename.encode!(Encoding::UTF_8) + rescue EncodingError + @original_filename.force_encoding(Encoding::UTF_8) + end + end @content_type = hash[:type] @headers = hash[:head] end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f5b709ccd6..6fcf49030b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -245,7 +245,7 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, '') + raw_host_with_port.sub(/:\d+$/, ''.freeze) end # Returns a \host:\port string for this request, such as "example.com" or diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index c0566c6fc9..c19ff0f4db 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -14,7 +14,7 @@ module ActionDispatch def generate(name, options, path_parameters, parameterize = nil) constraints = path_parameters.merge(options) - missing_keys = [] + missing_keys = nil # need for variable scope match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) @@ -25,22 +25,22 @@ module ActionDispatch next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) - next unless missing_keys.empty? + next if missing_keys && !missing_keys.empty? params = options.dup.delete_if do |key, _| parameterized_parts.key?(key) || route.defaults.key?(key) end defaults = route.defaults required_parts = route.required_parts - parameterized_parts.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) + parameterized_parts.keep_if do |key, value| + defaults[key].nil? || value.to_s != defaults[key].to_s || required_parts.include?(key) end 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}" unless missing_keys.empty? + message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? raise ActionController::UrlGenerationError, message end @@ -54,12 +54,12 @@ module ActionDispatch def extract_parameterized_parts(route, options, recall, parameterize = nil) parameterized_parts = recall.merge(options) - keys_to_keep = route.parts.reverse.drop_while { |part| + keys_to_keep = route.parts.reverse_each.drop_while { |part| !options.key?(part) || (options[part] || recall[part]).nil? } | route.required_parts - (parameterized_parts.keys - keys_to_keep).each do |bad_key| - parameterized_parts.delete(bad_key) + parameterized_parts.delete_if do |bad_key, _| + !keys_to_keep.include?(bad_key) end if parameterize @@ -110,15 +110,36 @@ module ActionDispatch routes end + module RegexCaseComparator + DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/ + DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/ + + def self.===(regex) + DEFAULT_INPUT == regex + end + end + # Returns an array populated with missing keys if any are present. def missing_keys(route, parts) - missing_keys = [] + missing_keys = nil tests = route.path.requirements route.required_parts.each { |key| - if tests.key?(key) - missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] + case tests[key] + when nil + unless parts[key] + missing_keys ||= [] + missing_keys << key + end + when RegexCaseComparator + unless RegexCaseComparator::DEFAULT_REGEX === parts[key] + missing_keys ||= [] + missing_keys << key + end else - missing_keys << key unless parts[key] + unless /\A#{tests[key]}\Z/ === parts[key] + missing_keys ||= [] + missing_keys << key + end end } missing_keys @@ -134,7 +155,7 @@ module ActionDispatch def build_cache root = { ___routes: [] } - routes.each_with_index do |route, i| + routes.routes.each_with_index do |route, i| leaf = route.required_defaults.inject(root) do |h, tuple| h[tuple] ||= {} end diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..542e8c6577 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -14,15 +14,15 @@ module ActionDispatch end def each(&block) - Visitors::Each.new(block).accept(self) + Visitors::Each::INSTANCE.accept(self, block) end def to_s - Visitors::String.new.accept(self) + Visitors::String::INSTANCE.accept(self, '') end def to_dot - Visitors::Dot.new.accept(self) + Visitors::Dot::INSTANCE.accept(self) end def to_sym @@ -30,7 +30,7 @@ module ActionDispatch end def name - left.tr '*:', '' + left.tr '*:'.freeze, ''.freeze end def type @@ -39,10 +39,13 @@ module ActionDispatch def symbol?; false; end def literal?; false; end + def terminal?; false; end + def star?; false; end end class Terminal < Node # :nodoc: alias :symbol :left + def terminal?; true; end end class Literal < Terminal # :nodoc: @@ -92,6 +95,7 @@ module ActionDispatch end class Star < Unary # :nodoc: + def star?; true; end def type; :STAR; end def name diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb index 14892f4321..fff0299812 100644 --- a/actionpack/lib/action_dispatch/journey/parser_extras.rb +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -6,6 +6,10 @@ module ActionDispatch class Parser < Racc::Parser # :nodoc: include Journey::Nodes + def self.parse(string) + new.parse string + end + def initialize @scanner = Scanner.new end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 64b48ca45f..e93970046c 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,5 +1,3 @@ -require 'action_dispatch/journey/router/strexp' - module ActionDispatch module Journey # :nodoc: module Path # :nodoc: @@ -7,14 +5,20 @@ module ActionDispatch attr_reader :spec, :requirements, :anchored def self.from_string string - new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + build(string, {}, "/.?", true) + end + + def self.build(path, requirements, separators, anchored) + parser = Journey::Parser.new + ast = parser.parse path + new ast, requirements, separators, anchored end - def initialize(strexp) - @spec = strexp.ast - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor + def initialize(ast, requirements, separators, anchored) + @spec = ast + @requirements = requirements + @separators = separators + @anchored = anchored @names = nil @optional_names = nil @@ -28,12 +32,12 @@ module ActionDispatch end def ast - @spec.grep(Nodes::Symbol).each do |node| + @spec.find_all(&:symbol?).each do |node| re = @requirements[node.to_sym] node.regexp = re if re end - @spec.grep(Nodes::Star).each do |node| + @spec.find_all(&:star?).each do |node| node = node.left node.regexp = @requirements[node.to_sym] || /(.+)/ end @@ -55,31 +59,6 @@ module ActionDispatch }.map(&:name).uniq end - class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: - attr_reader :offsets - - def initialize(matchers) - @matchers = matchers - @capture_count = [0] - end - - def visit(node) - super - @capture_count - end - - def visit_SYMBOL(node) - node = node.to_sym - - if @matchers.key?(node) - re = /#{@matchers[node]}|/ - @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0)) - else - @capture_count << (@capture_count.last || 0) - end - end - end - class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc: def initialize(separator, matchers) @separator = separator @@ -189,8 +168,20 @@ module ActionDispatch def offsets return @offsets if @offsets - viz = RegexpOffsets.new(@requirements) - @offsets = viz.accept(spec) + @offsets = [0] + + spec.find_all(&:symbol?).each do |node| + node = node.to_sym + + if @requirements.key?(node) + re = /#{@requirements[node]}|/ + @offsets.push((re.match('').length - 1) + @offsets.last) + else + @offsets << @offsets.last + end + end + + @offsets end end end diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 4698ff8cc7..209cd20d80 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -8,17 +8,65 @@ module ActionDispatch attr_accessor :precedence + 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 + eoc + end + + class Unknown + attr_reader :verb + + def initialize(verb) + @verb = verb + end + + def call(request); @verb === request.request_method; end + end + + class All + def self.call(_); true; end + def self.verb; ''; end + end + + VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash| + klass = const_get verb + hash[verb] = klass + hash[verb.downcase] = klass + hash[verb.downcase.to_sym] = klass + end + + end + + def self.verb_matcher(verb) + VerbMatchers::VERB_TO_CLASS.fetch(verb) do + VerbMatchers::Unknown.new verb.to_s.dasherize.upcase + end + end + + def self.build(name, app, path, constraints, required_defaults, defaults) + request_method_match = verb_matcher(constraints.delete(:request_method)) + new name, app, path, constraints, required_defaults, defaults, request_method_match + end + ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. - def initialize(name, app, path, constraints, defaults = {}) + def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match) @name = name @app = app @path = path + @request_method_match = request_method_match @constraints = constraints @defaults = defaults @required_defaults = nil + @_required_defaults = required_defaults @required_parts = nil @parts = nil @decorated_ast = nil @@ -29,7 +77,7 @@ module ActionDispatch def ast @decorated_ast ||= begin decorated_ast = path.ast - decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self } + decorated_ast.find_all(&:terminal?).each { |n| n.memo = self } decorated_ast end end @@ -73,7 +121,7 @@ module ActionDispatch end def required_default?(key) - (constraints[:required_defaults] || []).include?(key) + @_required_defaults.include?(key) end def required_defaults @@ -91,9 +139,8 @@ module ActionDispatch end def matches?(request) - constraints.all? do |method, value| - next true unless request.respond_to?(method) - + match_verb(request) && + constraints.all? { |method, value| case value when Regexp, String value === request.send(method).to_s @@ -106,15 +153,28 @@ module ActionDispatch else value === request.send(method) end - end + } end def ip constraints[:ip] || // end + def requires_matching_verb? + !@request_method_match.all? { |x| x == VerbMatchers::All } + end + def verb - constraints[:request_method] || // + %r[^#{verbs.join('|')}$] + end + + private + def verbs + @request_method_match.map(&:verb) + end + + def match_verb(request) + @request_method_match.any? { |m| m.call request } end end end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index cc4bd6105d..f649588520 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -1,5 +1,4 @@ require 'action_dispatch/journey/router/utils' -require 'action_dispatch/journey/router/strexp' require 'action_dispatch/journey/routes' require 'action_dispatch/journey/formatter' @@ -102,7 +101,7 @@ module ActionDispatch } routes = - if req.request_method == "HEAD" + if req.head? match_head_routes(routes, req) else match_routes(routes, req) @@ -121,7 +120,8 @@ module ActionDispatch end def match_head_routes(routes, req) - head_routes = match_routes(routes, req) + verb_specific_routes = routes.select(&:requires_matching_verb?) + head_routes = match_routes(verb_specific_routes, req) if head_routes.empty? begin diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb deleted file mode 100644 index 4b7738f335..0000000000 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActionDispatch - module Journey # :nodoc: - class Router # :nodoc: - class Strexp # :nodoc: - class << self - alias :compile :new - end - - attr_reader :path, :requirements, :separators, :anchor, :ast - - def self.build(path, requirements, separators, anchor = true) - parser = Journey::Parser.new - ast = parser.parse path - new ast, path, requirements, separators, anchor - end - - def initialize(ast, path, requirements, separators, anchor = true) - @ast = ast - @path = path - @requirements = requirements - @separators = separators - @anchor = anchor - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 2b0a6575d4..9793ca1c7a 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -14,10 +14,10 @@ module ActionDispatch # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" - path.squeeze!('/') - path.sub!(%r{/+\Z}, '') + path.squeeze!('/'.freeze) + path.sub!(%r{/+\Z}, ''.freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = '/' if path == '' + path = '/' if path == ''.freeze path end @@ -55,7 +55,7 @@ module ActionDispatch def unescape_uri(uri) encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding - uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding) + uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack('C') }.force_encoding(encoding) end protected diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index 6325dfa3dd..f7b009109e 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -5,11 +5,10 @@ module ActionDispatch class Routes # :nodoc: include Enumerable - attr_reader :routes, :named_routes, :custom_routes, :anchored_routes + attr_reader :routes, :custom_routes, :anchored_routes def initialize @routes = [] - @named_routes = {} @ast = nil @anchored_routes = [] @custom_routes = [] @@ -37,7 +36,6 @@ module ActionDispatch routes.clear anchored_routes.clear custom_routes.clear - named_routes.clear end def partition_route(route) @@ -62,13 +60,9 @@ module ActionDispatch end end - # Add a route to the routing table. - def add_route(app, path, conditions, defaults, name = nil) - route = Route.new(name, app, path, conditions, defaults) - - route.precedence = routes.length + def add_route(name, mapping) + route = mapping.make_route name, routes.length routes << route - named_routes[name] = route if name && !named_routes[name] partition_route(route) clear_cache! route diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 52b4c8b489..537c9b2f5c 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -92,6 +92,45 @@ module ActionDispatch end end + class FunctionalVisitor # :nodoc: + DISPATCH_CACHE = {} + + def accept(node, seed) + visit(node, seed) + end + + def visit node, seed + send(DISPATCH_CACHE[node.type], node, seed) + end + + def binary(node, seed) + visit(node.right, visit(node.left, seed)) + end + def visit_CAT(n, seed); binary(n, seed); end + + def nary(node, seed) + node.children.inject(seed) { |s, c| visit(c, s) } + end + def visit_OR(n, seed); nary(n, seed); end + + def unary(node, seed) + visit(node.left, seed) + end + def visit_GROUP(n, seed); unary(n, seed); end + def visit_STAR(n, seed); unary(n, seed); end + + def terminal(node, seed); seed; end + def visit_LITERAL(n, seed); terminal(n, seed); end + def visit_SYMBOL(n, seed); terminal(n, seed); end + def visit_SLASH(n, seed); terminal(n, seed); end + def visit_DOT(n, seed); terminal(n, seed); end + + instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim + end + end + class FormatBuilder < Visitor # :nodoc: def accept(node); Journey::Format.new(super); end def terminal(node); [node.left]; end @@ -117,104 +156,110 @@ module ActionDispatch end # Loop through the requirements AST - class Each < Visitor # :nodoc: - attr_reader :block - - def initialize(block) - @block = block - end - - def visit(node) + class Each < FunctionalVisitor # :nodoc: + def visit(node, block) block.call(node) super end + + INSTANCE = new end - class String < Visitor # :nodoc: + class String < FunctionalVisitor # :nodoc: private - def binary(node) - [visit(node.left), visit(node.right)].join + def binary(node, seed) + visit(node.right, visit(node.left, seed)) end - def nary(node) - node.children.map { |c| visit(c) }.join '|' + def nary(node, seed) + last_child = node.children.last + node.children.inject(seed) { |s, c| + string = visit(c, s) + string << "|".freeze unless last_child == c + string + } end - def terminal(node) - node.left + def terminal(node, seed) + seed + node.left end - def visit_GROUP(node) - "(#{visit(node.left)})" + def visit_GROUP(node, seed) + visit(node.left, seed << "(".freeze) << ")".freeze end + + INSTANCE = new end - class Dot < Visitor # :nodoc: + class Dot < FunctionalVisitor # :nodoc: def initialize @nodes = [] @edges = [] end - def accept(node) + def accept(node, seed = [[], []]) super + nodes, edges = seed <<-eodot digraph parse_tree { size="8,5" node [shape = none]; edge [dir = none]; - #{@nodes.join "\n"} - #{@edges.join("\n")} + #{nodes.join "\n"} + #{edges.join("\n")} } eodot end private - def binary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def binary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def nary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def nary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def unary(node) - @edges << "#{node.object_id} -> #{node.left.object_id};" + def unary(node, seed) + seed.last << "#{node.object_id} -> #{node.left.object_id};" super end - def visit_GROUP(node) - @nodes << "#{node.object_id} [label=\"()\"];" + def visit_GROUP(node, seed) + seed.first << "#{node.object_id} [label=\"()\"];" super end - def visit_CAT(node) - @nodes << "#{node.object_id} [label=\"○\"];" + def visit_CAT(node, seed) + seed.first << "#{node.object_id} [label=\"○\"];" super end - def visit_STAR(node) - @nodes << "#{node.object_id} [label=\"*\"];" + def visit_STAR(node, seed) + seed.first << "#{node.object_id} [label=\"*\"];" super end - def visit_OR(node) - @nodes << "#{node.object_id} [label=\"|\"];" + def visit_OR(node, seed) + seed.first << "#{node.object_id} [label=\"|\"];" super end - def terminal(node) + def terminal(node, seed) value = node.left - @nodes << "#{node.object_id} [label=\"#{value}\"];" + seed.first << "#{node.object_id} [label=\"#{value}\"];" + seed end + INSTANCE = new end end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index dd1f140051..cf4f654ed6 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -8,8 +8,50 @@ require 'active_support/json' module ActionDispatch class Request < Rack::Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + env['action_dispatch.cookies'.freeze] ||= Cookies::CookieJar.build(self, cookies) end + + # :stopdoc: + def have_cookie_jar? + env.key? 'action_dispatch.cookies'.freeze + end + + def cookie_jar=(jar) + env['action_dispatch.cookies'.freeze] = jar + end + + def key_generator + env[Cookies::GENERATOR_KEY] + end + + def signed_cookie_salt + env[Cookies::SIGNED_COOKIE_SALT] + end + + def encrypted_cookie_salt + env[Cookies::ENCRYPTED_COOKIE_SALT] + end + + def encrypted_signed_cookie_salt + env[Cookies::ENCRYPTED_SIGNED_COOKIE_SALT] + end + + def secret_token + env[Cookies::SECRET_TOKEN] + end + + def secret_key_base + env[Cookies::SECRET_KEY_BASE] + end + + def cookies_serializer + env[Cookies::COOKIES_SERIALIZER] + end + + def cookies_digest + env[Cookies::COOKIES_DIGEST] + end + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -118,7 +160,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -138,10 +180,10 @@ module ActionDispatch # cookies.signed[:discount] # => 45 def signed @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacySignedCookieJar.new(self) else - SignedCookieJar.new(self, @key_generator, @options) + SignedCookieJar.new(self) end end @@ -161,10 +203,10 @@ module ActionDispatch # cookies.encrypted[:discount] # => 45 def encrypted @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacyEncryptedCookieJar.new(self) else - EncryptedCookieJar.new(self, @key_generator, @options) + EncryptedCookieJar.new(self) end end @@ -172,12 +214,26 @@ module ActionDispatch # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end + + protected + + def request; @parent_jar.request; end + + private + + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end + + def key_generator + request.key_generator + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +243,7 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -216,38 +272,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(request) - env = request.env - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + def self.build(req, cookies) + new(req).tap do |hash| + hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -283,6 +319,10 @@ module ActionDispatch self end + def to_header + @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + end + def handle_options(options) #:nodoc: options[:path] ||= "/" @@ -292,12 +332,12 @@ module ActionDispatch # if host is not ip and matches domain regexp # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + 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 - options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } + options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') } end end @@ -356,27 +396,20 @@ module ActionDispatch @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} - end - mattr_accessor :always_write_cookie self.always_write_cookie = false private def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie + request.ssl? || !cookie[:secure] || always_write_cookie end end class PermanentCookieJar #:nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) @@ -410,7 +443,7 @@ module ActionDispatch protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) @@ -430,7 +463,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -442,7 +475,7 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || 'SHA1' end end @@ -450,10 +483,9 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:signed_cookie_salt]) + secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -505,16 +537,16 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) + @parent_jar = parent_jar + if ActiveSupport::LegacyKeyGenerator === key_generator 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 - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) - sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + secret = key_generator.generate_key(request.encrypted_cookie_salt || '') + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -568,9 +600,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..226a688fe2 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -44,6 +44,7 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers['X-Cascade'] == 'pass' @@ -53,14 +54,15 @@ module ActionDispatch response rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false + raise exception unless request.show_exceptions? render_exception(env, exception) end private def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) log_error(env, wrapper) if env['action_dispatch.show_detailed_exceptions'] diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index d176a73633..039efc3af8 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -17,7 +17,9 @@ module ActionDispatch 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, 'ActionDispatch::ParamsParser::ParseError' => :bad_request, 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::Utils::ParameterTypeError' => :bad_request, + 'Rack::Utils::InvalidParameterError' => :bad_request ) cattr_accessor :rescue_templates @@ -29,10 +31,10 @@ module ActionDispatch 'ActionView::Template::Error' => 'template_error' ) - attr_reader :env, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :line_number, :file - def initialize(env, exception) - @env = env + def initialize(backtrace_cleaner, exception) + @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) @@ -123,10 +125,6 @@ module ActionDispatch end end - def backtrace_cleaner - @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] - end - def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..23da169b22 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -8,6 +8,14 @@ module ActionDispatch def flash @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) end + + def flash=(flash) + @env[Flash::KEY] = flash + end + + def flash_hash # :nodoc: + @env[Flash::KEY] + end end # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed @@ -263,14 +271,15 @@ module ActionDispatch end def call(env) + req = ActionDispatch::Request.new env @app.call(env) ensure session = Request::Session.find(env) || {} - flash_hash = env[KEY] + flash_hash = req.flash_hash if flash_hash && (flash_hash.present? || session.key?('flash')) session["flash"] = flash_hash.to_session_value - env[KEY] = flash_hash.dup + req.flash = flash_hash.dup end if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb new file mode 100644 index 0000000000..07f498319c --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/load_interlock.rb @@ -0,0 +1,21 @@ +require 'active_support/dependencies' +require 'rack/body_proxy' + +module ActionDispatch + class LoadInterlock + def initialize(app) + @app = app + end + + def call(env) + interlock = ActiveSupport::Dependencies.interlock + interlock.start_running + response = @app.call(env) + body = Rack::BodyProxy.new(response[2]) { interlock.done_running } + response[2] = body + response + ensure + interlock.done_running unless body + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..402ad778fa 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,9 +1,14 @@ -require 'active_support/core_ext/hash/conversions' require 'action_dispatch/http/request' -require 'active_support/core_ext/hash/indifferent_access' 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 attr_reader :original_exception @@ -13,48 +18,46 @@ module ActionDispatch end end - DEFAULT_PARSERS = { Mime::JSON => :json } - + DEFAULT_PARSERS = { + Mime::JSON => lambda { |raw_post| + data = ActiveSupport::JSON.decode(raw_post) + data = {:_json => data} unless data.is_a?(Hash) + Request::Utils.normalize_encode_params(data) + } + } + + # 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 initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) end def call(env) - if params = parse_formatted_parameters(env) - env["action_dispatch.request.request_parameters"] = params - end + request = Request.new(env) + + request.request_parameters = parse_formatted_parameters(request, @parsers) @app.call(env) end private - def parse_formatted_parameters(env) - request = Request.new(env) - - return false if request.content_length.zero? + def parse_formatted_parameters(request, parsers) + return if request.content_length.zero? - strategy = @parsers[request.content_mime_type] + strategy = parsers.fetch(request.content_mime_type) { return nil } - return false unless strategy + strategy.call(request.raw_post) - case strategy - when Proc - strategy.call(request.raw_post) - when :json - data = ActiveSupport::JSON.decode(request.raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access - else - false - end rescue => e # JSON or Ruby code block errors - logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" + logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" raise ParseError.new(e.message, e) end - def logger(env) - env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) + def logger(request) + request.logger || ActiveSupport::Logger.new($stderr) end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 7c4236518d..aee2334da9 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -74,18 +74,19 @@ module ActionDispatch # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, self) - @app.call(env) + req = ActionDispatch::Request.new env + req.remote_ip = GetIp.new(req, check_ip, proxies) + @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp - def initialize(env, middleware) - @env = env - @check_ip = middleware.check_ip - @proxies = middleware.proxies + def initialize(req, check_ip, proxies) + @req = req + @check_ip = check_ip + @proxies = proxies end # Sort through the various IP address headers, looking for the IP most @@ -108,11 +109,11 @@ module ActionDispatch # the last address left, which was presumably set by one of those proxies. def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from('REMOTE_ADDR').last + remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from('HTTP_CLIENT_IP').reverse - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse + client_ips = ips_from(@req.client_ip).reverse + forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. # If they are both set, it means that this request passed through two @@ -123,8 +124,8 @@ module ActionDispatch 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=#{@env['HTTP_CLIENT_IP'].inspect} " + - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: @@ -147,8 +148,9 @@ module ActionDispatch protected def ips_from(header) + return [] unless header # Split the comma-separated list into an array of strings - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..12d8dab8eb 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,19 +27,21 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env @app.call(env) rescue Exception => exception - if env['action_dispatch.show_exceptions'] == false - raise exception - else + if request.show_exceptions? render_exception(env, exception) + else + raise exception end end private def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception env["action_dispatch.original_path"] = env["PATH_INFO"] diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 0c7caef25d..7b3d8bcc5b 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -22,7 +22,7 @@ module ActionDispatch if request.ssl? status, headers, body = @app.call(env) - headers = hsts_headers.merge(headers) + headers.reverse_merge!(hsts_headers) flag_cookies_as_secure!(headers) [status, headers, body] else diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..0430ce3b9a 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,36 +4,15 @@ require "active_support/dependencies" module ActionDispatch class MiddlewareStack class Middleware - attr_reader :args, :block, :name, :classcache + attr_reader :args, :block, :klass - def initialize(klass_or_name, *args, &block) - @klass = nil - - if klass_or_name.respond_to?(:name) - @klass = klass_or_name - @name = @klass.name - else - @name = klass_or_name.to_s - end - - @classcache = ActiveSupport::Dependencies::Reference - @args, @block = args, block + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block end - def klass - @klass || classcache[@name] - end - - def ==(middleware) - case middleware - when Middleware - klass == middleware.klass - when Class - klass == middleware - else - normalize(@name) == normalize(middleware) - end - end + def name; klass.name; end def inspect klass.to_s @@ -42,12 +21,6 @@ module ActionDispatch def build(app) klass.new(app, *args, &block) end - - private - - def normalize(object) - object.to_s.strip.sub(/^::/, '') - end end include Enumerable @@ -75,19 +48,17 @@ module ActionDispatch middlewares[i] end - def unshift(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.unshift(middleware) + def unshift(klass, *args, &block) + middlewares.unshift(build_middleware(klass, args, block)) end def initialize_copy(other) self.middlewares = other.middlewares.dup end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, build_middleware(klass, args, block)) end alias_method :insert_before, :insert @@ -104,26 +75,48 @@ module ActionDispatch end def delete(target) - middlewares.delete target + target = get_class target + middlewares.delete_if { |m| m.klass == target } end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.push(middleware) + def use(klass, *args, &block) + middlewares.push(build_middleware(klass, args, block)) end - def build(app = nil, &block) - app ||= block - raise "MiddlewareStack#build requires an app" unless app + def build(app = Proc.new) middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : middlewares.index(index) + 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 + + private + + 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) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index f20f6ca865..9462ae4278 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -13,7 +13,7 @@ module ActionDispatch # located at `public/assets/application.js` if the file exists. If the file # does not exist, a 404 "File not Found" response will be returned. class FileHandler - def initialize(root, cache_control, index) + def initialize(root, cache_control, index: 'index') @root = root.chomp('/') @compiled_root = /^#{Regexp.escape(root)}/ headers = cache_control && { 'Cache-Control' => cache_control } @@ -21,7 +21,6 @@ module ActionDispatch @index = index end - # Takes a path to a file. If the file is found, has valid encoding, and has # correct read permissions, the return value is a URI-escaped string # representing the filename. Otherwise, false is returned. @@ -36,7 +35,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding('UTF-8')) + path = File.join(@root, p.force_encoding('UTF-8'.freeze)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -49,26 +48,30 @@ module ActionDispatch end def call(env) - path = env['PATH_INFO'] + serve ActionDispatch::Request.new env + end + + def serve(request) + path = request.path_info gzip_path = gzip_file_path(path) - if gzip_path && gzip_encoding_accepted?(env) - env['PATH_INFO'] = gzip_path - status, headers, body = @file_server.call(env) + if gzip_path && gzip_encoding_accepted?(request) + request.path_info = gzip_path + status, headers, body = @file_server.call(request.env) if status == 304 return [status, headers, body] end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else - status, headers, body = @file_server.call(env) + status, headers, body = @file_server.call(request.env) end headers['Vary'] = 'Accept-Encoding' if gzip_path return [status, headers, body] ensure - env['PATH_INFO'] = path + request.path_info = path end private @@ -77,11 +80,11 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') + ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end - def gzip_encoding_accepted?(env) - env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i + def gzip_encoding_accepted?(request) + request.accept_encoding =~ /\bgzip\b/i end def gzip_file_path(path) @@ -105,22 +108,23 @@ 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, cache_control=nil, index="index") + def initialize(app, path, cache_control = nil, index: 'index') @app = app - @file_handler = FileHandler.new(path, cache_control, index) + @file_handler = FileHandler.new(path, cache_control, index: index) end def call(env) - case env['REQUEST_METHOD'] - when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + req = ActionDispatch::Request.new env + + if req.get? || req.head? + path = req.path_info.chomp('/'.freeze) if match = @file_handler.match?(path) - env["PATH_INFO"] = match - return @file_handler.call(env) + req.path_info = match + return @file_handler.serve(req) end end - @app.call(env) + @app.call(req.env) end end end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 9a1a05e971..a8a3cd20b9 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -17,7 +17,7 @@ module ActionDispatch session.merge! session_was if session_was set(env, session) - Options.set(env, Request::Session::Options.new(store, env, default_options)) + Options.set(env, Request::Session::Options.new(store, default_options)) session end @@ -38,20 +38,19 @@ module ActionDispatch env[ENV_SESSION_OPTIONS_KEY] end - def initialize(by, env, default_options) + def initialize(by, default_options) @by = by - @env = env @delegate = default_options.dup end def [](key) - if key == :id - @delegate.fetch(key) { - @delegate[:id] = @by.send(:extract_session_id, @env) - } - else - @delegate[key] - end + @delegate[key] + end + + def id(env) + @delegate.fetch(:id) { + @by.send(:extract_session_id, env) + } end def []=(k,v); @delegate[k] = v; end @@ -68,7 +67,7 @@ module ActionDispatch end def id - options[:id] + options.id(@env) end def options @@ -78,19 +77,21 @@ module ActionDispatch def destroy clear options = self.options || {} - new_sid = @by.send(:destroy_session, @env, options[:id], options) - options[:id] = new_sid # Reset session id with a new value or nil + @by.send(:destroy_session, @env, options.id(@env), options) # 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. def [](key) load_for_read! @delegate[key.to_s] end + # Returns true if the session has the given key or false. def has_key?(key) load_for_read! @delegate.key?(key.to_s) @@ -98,39 +99,69 @@ module ActionDispatch alias :key? :has_key? alias :include? :has_key? + # Returns keys of the session as Array. def keys @delegate.keys end + # Returns values of the session as Array. def values @delegate.values end + # Writes given value to given key of the session. def []=(key, value) load_for_write! @delegate[key.to_s] = value end + # Clears the session. def clear load_for_write! @delegate.clear end + # Returns the session as Hash. def to_hash load_for_read! @delegate.dup.delete_if { |_,v| v.nil? } end + # Updates the session with given Hash. + # + # session.to_hash + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"} + # + # session.update({ "foo" => "bar" }) + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} + # + # session.to_hash + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} def update(hash) load_for_write! @delegate.update stringify_keys(hash) end + # Deletes given key from the session. def delete(key) load_for_write! @delegate.delete key.to_s end + # Returns value of given key from the session, or raises +KeyError+ + # if can't find given key in case of not setted dafault value. + # Returns default value if specified. + # + # session.fetch(:foo) + # # => KeyError: key not found: "foo" + # + # session.fetch(:foo, :bar) + # # => :bar + # + # session.fetch(:foo) do + # :bar + # end + # # => :bar def fetch(key, default=Unspecified, &block) load_for_read! if default == Unspecified diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 1c9371d89c..3973ea6346 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -5,24 +5,45 @@ module ActionDispatch mattr_accessor :perform_deep_munge self.perform_deep_munge = true - class << self - # Remove nils from the params hash - def deep_munge(hash, keys = []) - return hash unless perform_deep_munge + def self.normalize_encode_params(params) + if perform_deep_munge + NoNilParamEncoder.normalize_encode_params params + else + ParamEncoder.normalize_encode_params params + end + end - hash.each do |k, v| - keys << k - case v - when Array - v.grep(Hash) { |x| deep_munge(x, keys) } - v.compact! - when Hash - deep_munge(v, keys) + class ParamEncoder # :nodoc: + # Convert nested Hash to HashWithIndifferentAccess. + # + def self.normalize_encode_params(params) + case params + when Array + handle_array params + when Hash + if params.has_key?(:tempfile) + ActionDispatch::Http::UploadedFile.new(params) + else + params.each_with_object({}) do |(key, val), new_hash| + new_hash[key] = normalize_encode_params(val) + end.with_indifferent_access end - keys.pop + else + params end + end + + def self.handle_array(params) + params.map! { |el| normalize_encode_params(el) } + end + end - hash + # Remove nils from the params hash + class NoNilParamEncoder < ParamEncoder # :nodoc: + def self.handle_array(params) + list = super + list.compact! + list end end end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index ce03164ca9..a42cf72f60 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -232,7 +232,6 @@ module ActionDispatch # def send_to_jail # get '/jail' # assert_response :success - # assert_template "jail/front" # end # # def goes_to_login diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0a444ddffc..0afe1852f2 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' +require 'active_support/deprecation' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' @@ -16,7 +17,10 @@ module ActionDispatch class Constraints < Endpoint #:nodoc: attr_reader :app, :constraints - def initialize(app, constraints, dispatcher_p) + SERVE = ->(app, req) { app.serve req } + CALL = ->(app, req) { app.call req.env } + + def initialize(app, constraints, strategy) # 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 @@ -26,12 +30,12 @@ module ActionDispatch app = app.app end - @dispatcher = dispatcher_p + @strategy = strategy @app, @constraints, = app, constraints end - def dispatcher?; @dispatcher; end + def dispatcher?; @strategy == SERVE; end def matches?(req) @constraints.all? do |constraint| @@ -43,11 +47,7 @@ module ActionDispatch def serve(req) return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) - if dispatcher? - @app.serve req - else - @app.call req.env - end + @strategy.call @app, req end private @@ -59,101 +59,170 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - attr_reader :requirements, :conditions, :defaults - attr_reader :to, :default_controller, :default_action, :as, :anchor + attr_reader :requirements, :defaults + attr_reader :to, :default_controller, :default_action + attr_reader :required_defaults, :ast - def self.build(scope, set, path, as, options) + def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) options = scope[:options].merge(options) if scope[:options] - options.delete :only - options.delete :except - options.delete :shallow_path - options.delete :shallow_prefix - options.delete :shallow + defaults = (scope[:defaults] || {}).dup + scope_constraints = scope[:constraints] || {} - defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options + end - new scope, set, path, defaults, as, options + def self.check_via(via) + if via.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + via end - def initialize(scope, set, path, defaults, as, options) - @requirements, @conditions = {}, {} - @defaults = defaults - @set = set + def self.normalize_path(path, format) + path = Mapper.normalize_path(path) - @to = options.delete :to - @default_controller = options.delete(:controller) || scope[:controller] - @default_action = options.delete(:action) || scope[:action] - @as = as - @anchor = options.delete :anchor + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end - formatted = options.delete :format - via = Array(options.delete(:via) { [] }) - options_constraints = options.delete :constraints + def self.optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') + end - path = normalize_path! path, formatted - ast = path_ast path - path_params = path_params ast + def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) + @defaults = defaults + @set = set + + @to = to + @default_controller = controller + @default_action = default_action + @ast = ast + @anchor = anchor + @via = via - options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + path_params = ast.find_all(&:symbol?).map(&:to_sym) + options = add_wildcard_options(options, formatted, ast) - split_constraints(path_params, scope[:constraints]) if scope[:constraints] - constraints = constraints(options, path_params) + options = normalize_options!(options, path_params, modyoule) - split_constraints path_params, constraints + split_options = constraints(options, path_params) - @blocks = blocks(options_constraints, scope[:blocks]) + constraints = scope_constraints.merge Hash[split_options[:constraints] || []] if options_constraints.is_a?(Hash) - split_constraints path_params, options_constraints - options_constraints.each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end + @defaults = Hash[options_constraints.find_all { |key, default| + URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + }].merge @defaults + @blocks = blocks + constraints.merge! options_constraints + else + @blocks = blocks(options_constraints) end - normalize_format!(formatted) + requirements, conditions = split_constraints path_params, constraints + verify_regexp_requirements requirements.map(&:last).grep(Regexp) + + formats = normalize_format(formatted) - @conditions[:path_info] = path - @conditions[:parsed_path_info] = ast + @requirements = formats[:requirements].merge Hash[requirements] + @conditions = Hash[conditions] + @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) - add_request_method(via, @conditions) - normalize_defaults!(options) + @required_defaults = (split_options[:required_defaults] || []).map(&:first) end - def to_route - [ app(@blocks), conditions, requirements, defaults, as, anchor ] + def make_route(name, precedence) + route = Journey::Route.new(name, + application, + path, + conditions, + required_defaults, + defaults, + request_method) + + route.precedence = precedence + route end - private + def application + app(@blocks) + end + + def path + build_path @ast, requirements, @anchor + end - def normalize_path!(path, format) - path = Mapper.normalize_path(path) + def conditions + build_conditions @conditions, @set.request_class + end - if format == true - "#{path}.:format" - elsif optional_format?(path, format) - "#{path}(.:format)" - else - path - end - end + def build_conditions(current_conditions, request_class) + conditions = current_conditions.dup - def optional_format?(path, format) - format != false && !path.include?(':format') && !path.end_with?('/') + conditions.keep_if do |k, _| + request_class.public_method_defined?(k) end + end + private :build_conditions + + def request_method + @via.map { |x| Journey::Route.verb_matcher(x) } + end + private :request_method + + JOINED_SEPARATORS = SEPARATORS.join # :nodoc: + + def build_path(ast, requirements, anchor) + pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) + + builder = Journey::GTG::Builder.new ast - def normalize_options!(options, formatted, path_params, path_ast, modyoule) + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = ast.grep(Journey::Nodes::Symbol).find_all { |n| + builder.followpos(n).first.literal? + } + + # Get all the symbol nodes preceded by literals. + symbols.concat ast.find_all(&:literal?).map { |n| + builder.followpos(n).first + }.find_all(&:symbol?) + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern + end + private :build_path + + + 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 if formatted != false - path_ast.grep(Journey::Nodes::Star) do |node| - options[node.name.to_sym] ||= /.+?/ - end + path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| + hash[node.name.to_sym] ||= /.+?/ + }.merge options + else + options end + end + def normalize_options!(options, path_params, modyoule) if path_params.include?(:controller) raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule @@ -178,74 +247,50 @@ module ActionDispatch end def split_constraints(path_params, constraints) - constraints.each_pair do |key, requirement| - if path_params.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement - else - @conditions[key] = requirement - end + constraints.partition do |key, requirement| + path_params.include?(key) || key == :controller end end - def normalize_format!(formatted) - if formatted == true - @requirements[:format] ||= /.+/ - elsif Regexp === formatted - @requirements[:format] = formatted - @defaults[:format] = nil - elsif String === formatted - @requirements[:format] = Regexp.compile(formatted) - @defaults[:format] = formatted - end - end - - def verify_regexp_requirement(requirement) - if requirement.source =~ ANCHOR_CHARACTERS_REGEX - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - - if requirement.multiline? - raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + def normalize_format(formatted) + case formatted + when true + { requirements: { format: /.+/ }, + defaults: {} } + when Regexp + { requirements: { format: formatted }, + defaults: { format: nil } } + when String + { requirements: { format: Regexp.compile(formatted) }, + defaults: { format: formatted } } + else + { requirements: { }, defaults: { } } end end - def normalize_defaults!(options) - options.each_pair do |key, default| - unless Regexp === default - @defaults[key] = default + def verify_regexp_requirements(requirements) + requirements.each do |requirement| + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end - end - end - def verify_callable_constraint(callable_constraint) - unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) - raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + end end end - def add_request_method(via, conditions) - return if via == [:all] - - if via.empty? - msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ - "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n" \ - " Instead of: match \"controller#action\"\n" \ - " Do: get \"controller#action\"" - raise ArgumentError, msg - end - - conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + def normalize_defaults(options) + Hash[options.reject { |_, default| Regexp === default }] end def app(blocks) if to.respond_to?(:call) - Constraints.new(to, blocks, false) + Constraints.new(to, blocks, Constraints::CALL) elsif blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - dispatcher(defaults) + dispatcher(defaults.key?(:controller)) end end @@ -303,40 +348,29 @@ module ActionDispatch yield end - def blocks(options_constraints, scope_blocks) - if options_constraints && !options_constraints.is_a?(Hash) - verify_callable_constraint(options_constraints) - [options_constraints] - else - scope_blocks || [] + def blocks(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + [callable_constraint] end def constraints(options, path_params) - constraints = {} - required_defaults = [] - options.each_pair do |key, option| + options.group_by do |key, option| if Regexp === option - constraints[key] = option + :constraints else - required_defaults << key unless path_params.include?(key) + if path_params.include?(key) + :path_params + else + :required_defaults + end end end - @conditions[:required_defaults] = required_defaults - constraints - end - - def path_params(ast) - ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } - end - - def path_ast(path) - parser = Journey::Parser.new - parser.parse path end - def dispatcher(defaults) - @set.dispatcher defaults + def dispatcher(raise_on_name_error) + @set.dispatcher raise_on_name_error end end @@ -443,6 +477,21 @@ module ActionDispatch # dynamic segment used to generate the routes). # You can access that segment from your controller using # <tt>params[<:param>]</tt>. + # In your router: + # + # resources :user, param: :name + # + # You can override <tt>ActiveRecord::Base#to_param</tt> of a related + # model to construct an URL: + # + # class User < ActiveRecord::Base + # def to_param + # name + # end + # end + # + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -670,7 +719,11 @@ module ActionDispatch def map_method(method, args, &block) options = args.extract_options! options[:via] = method - match(*args, options, &block) + if options.key?(:defaults) + defaults(options.delete(:defaults)) { match(*args, options, &block) } + else + match(*args, options, &block) + end self end end @@ -773,8 +826,8 @@ module ActionDispatch end if options[:constraints].is_a?(Hash) - defaults = options[:constraints].select do - |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + defaults = options[:constraints].select do |k, v| + URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) end (options[:defaults] ||= {}).reverse_merge!(defaults) @@ -782,16 +835,25 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end + if options.key?(:only) || options.key?(:except) + scope[:action_options] = { only: options.delete(:only), + except: options.delete(:except) } + end + + if options.key? :anchor + raise ArgumentError, 'anchor is ignored unless passed to `match`' + end + @scope.options.each do |option| if option == :blocks value = block elsif option == :options value = options else - value = options.delete(option) + value = options.delete(option) { POISON } end - if value + unless POISON == value scope[option] = send("merge_#{option}_scope", @scope[option], value) end end @@ -803,14 +865,18 @@ module ActionDispatch @scope = @scope.parent end + POISON = Object.new # :nodoc: + # Scopes routes to a specific controller # # controller "food" do - # match "bacon", action: "bacon" + # match "bacon", action: :bacon, via: :get # end - def controller(controller, options={}) - options[:controller] = controller - scope(options) { yield } + def controller(controller) + @scope = @scope.new(controller: controller) + yield + ensure + @scope = @scope.parent end # Scopes routes to a specific namespace. For example: @@ -856,13 +922,14 @@ module ActionDispatch defaults = { module: path, - path: options.fetch(:path, path), as: options.fetch(:as, path), shallow_path: options.fetch(:path, path), shallow_prefix: options.fetch(:as, path) } - scope(defaults.merge!(options)) { yield } + path_scope(options.delete(:path) { path }) do + scope(defaults.merge!(options)) { yield } + end end # === Parameter Restriction @@ -930,7 +997,10 @@ module ActionDispatch # end # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) - scope(:defaults => defaults) { yield } + @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults)) + yield + ensure + @scope = @scope.parent end private @@ -962,6 +1032,14 @@ module ActionDispatch child end + def merge_via_scope(parent, child) #:nodoc: + child + end + + def merge_format_scope(parent, child) #:nodoc: + child + end + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end @@ -981,16 +1059,12 @@ module ActionDispatch end def merge_options_scope(parent, child) #:nodoc: - (parent || {}).except(*override_keys(child)).merge!(child) + (parent || {}).merge(child) end def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - - def override_keys(child) #:nodoc: - child.key?(:only) || child.key?(:except) ? [:only, :except] : [] - end end # Resource routing allows you to quickly declare all of the common routes @@ -1040,27 +1114,34 @@ module ActionDispatch CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - attr_reader :controller, :path, :options, :param + attr_reader :controller, :path, :param - def initialize(entities, options = {}) + def initialize(entities, api_only, shallow, options = {}) @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @as = options[:as] @param = (options[:param] || :id).to_sym @options = options - @shallow = false + @shallow = shallow + @api_only = api_only + @only = options.delete :only + @except = options.delete :except end def default_actions - [:index, :create, :new, :show, :update, :destroy, :edit] + if @api_only + [:index, :create, :show, :update, :destroy] + else + [:index, :create, :new, :show, :update, :destroy, :edit] + end end def actions - if only = @options[:only] - Array(only).map(&:to_sym) - elsif except = @options[:except] - default_actions - Array(except).map(&:to_sym) + if @only + Array(@only).map(&:to_sym) + elsif @except + default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1087,7 +1168,7 @@ module ActionDispatch end def resource_scope - { :controller => controller } + controller end alias :collection_scope :path @@ -1110,17 +1191,15 @@ module ActionDispatch "#{path}/:#{nested_param}" end - def shallow=(value) - @shallow = value - end - def shallow? @shallow end + + def singleton?; false; end end class SingletonResource < Resource #:nodoc: - def initialize(entities, options) + def initialize(entities, api_only, shallow, options) super @as = nil @controller = (options[:controller] || plural).to_s @@ -1128,7 +1207,11 @@ module ActionDispatch end def default_actions - [:show, :create, :update, :destroy, :new, :edit] + if @api_only + [:show, :create, :update, :destroy] + else + [:show, :create, :update, :destroy, :new, :edit] + end end def plural @@ -1144,6 +1227,8 @@ module ActionDispatch alias :member_scope :path alias :nested_scope :path + + def singleton?; true; end end def resources_path_names(options) @@ -1178,20 +1263,23 @@ module ActionDispatch return self end - resource_scope(:resource, SingletonResource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resource) do + options = apply_action_options options + resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - post :create - end if parent_resource.actions.include?(:create) + collection do + post :create + end if parent_resource.actions.include?(:create) - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1336,21 +1424,24 @@ module ActionDispatch return self end - resource_scope(:resources, Resource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resources) do + options = apply_action_options options + resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - get :index if parent_resource.actions.include?(:index) - post :create if parent_resource.actions.include?(:create) - end + collection do + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) + end - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1374,7 +1465,7 @@ module ActionDispatch end with_scope_level(:collection) do - scope(parent_resource.collection_scope) do + path_scope(parent_resource.collection_scope) do yield end end @@ -1398,9 +1489,11 @@ module ActionDispatch with_scope_level(:member) do if shallow? - shallow_scope(parent_resource.member_scope) { yield } + shallow_scope { + path_scope(parent_resource.member_scope) { yield } + } else - scope(parent_resource.member_scope) { yield } + path_scope(parent_resource.member_scope) { yield } end end end @@ -1411,7 +1504,7 @@ module ActionDispatch end with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do + path_scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -1424,9 +1517,15 @@ module ActionDispatch with_scope_level(:nested) do if shallow? && shallow_nesting_depth >= 1 - shallow_scope(parent_resource.nested_scope, nested_options) { yield } + shallow_scope do + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end + end else - scope(parent_resource.nested_scope, nested_options) { yield } + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end end end end @@ -1441,18 +1540,22 @@ module ActionDispatch end def shallow - scope(:shallow => true) do - yield - end + @scope = @scope.new(shallow: true) + yield + ensure + @scope = @scope.parent end def shallow? - parent_resource.instance_of?(Resource) && @scope[:shallow] + !parent_resource.singleton? && @scope[:shallow] end - # match 'path' => 'controller#action' - # match 'path', to: 'controller#action' - # match 'path', 'otherpath', on: :member, via: :get + # Matches a url pattern to one or more routes. + # For more information, see match[rdoc-ref:Base#match]. + # + # match 'path' => 'controller#action', via: patch + # match 'path', to: 'controller#action', via: :post + # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest) if rest.empty? && Hash === path options = path @@ -1478,8 +1581,6 @@ module ActionDispatch paths = [path] + rest end - options[:anchor] = true unless options.key?(:anchor) - if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end @@ -1488,48 +1589,85 @@ module ActionDispatch options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" end - paths.each do |_path| + controller = options.delete(:controller) || @scope[:controller] + option_path = options.delete :path + to = options.delete :to + via = Mapping.check_via Array(options.delete(:via) { + @scope[:via] + }) + formatted = options.delete(:format) { @scope[:format] } + anchor = options.delete(:anchor) { true } + options_constraints = options.delete(:constraints) || {} + + path_types = paths.group_by(&:class) + path_types.fetch(String, []).each do |_path| route_options = options.dup - route_options[:path] ||= _path if _path.is_a?(String) + 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} - path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, route_options) - route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - route_options[:to].tr!("-", "_") +to this: + + match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect} + eowarn + route_options[:action] = _path + route_options[:as] = _path + _path = option_path end + to = get_to_from_path(_path, to, route_options[:action]) + decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) + end - decomposed_match(_path, route_options) + path_types.fetch(Symbol, []).each do |action| + route_options = options.dup + decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints) end + self end - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$} + def get_to_from_path(path, to, action) + return to if to || action + + path_without_format = path.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format) + path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_") + else + nil + end + end + + def using_match_shorthand?(path) + path =~ %r{^/?[-\w]+/[-\w/]+$} end - def decomposed_match(path, options) # :nodoc: + def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: if on = options.delete(:on) - send(on) { decomposed_match(path, options) } + send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else case @scope.scope_level when :resources - nested { decomposed_match(path, options) } + nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } when :resource - member { decomposed_match(path, options) } + member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else - add_route(path, options) + add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints) end end end - def add_route(action, options) # :nodoc: - path = path_for_action(action, options.delete(:path)) + def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + path = path_for_action(action, _path) raise ArgumentError, "path is required" if path.blank? - action = action.to_s.dup + action = action.to_s + + default_action = options.delete(:action) || @scope[:action] if action =~ /^[\w\-\/]+$/ - options[:action] ||= action.tr('-', '_') unless action.include?("/") + default_action ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1540,9 +1678,11 @@ module ActionDispatch name_for_action(options.delete(:as), action) end - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) + path = Mapping.normalize_path URI.parser.escape(path), formatted + 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) end def root(path, options={}) @@ -1556,7 +1696,7 @@ module ActionDispatch if @scope.resources? with_scope_level(:root) do - scope(parent_resource.path) do + path_scope(parent_resource.path) do super(options) end end @@ -1601,23 +1741,20 @@ module ActionDispatch return true end - unless action_options?(options) - options.merge!(scope_action_options) if scope_action_options? - end - false end - def action_options?(options) #:nodoc: - options[:only] || options[:except] + def apply_action_options(options) # :nodoc: + return options if action_options? options + options.merge scope_action_options end - def scope_action_options? #:nodoc: - @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) + def action_options?(options) #:nodoc: + options[:only] || options[:except] end def scope_action_options #:nodoc: - @scope[:options].slice(:only, :except) + @scope[:action_options] || {} end def resource_scope? #:nodoc: @@ -1632,18 +1769,6 @@ module ActionDispatch @scope.nested? end - def with_exclusive_scope - begin - @scope = @scope.new(:as => nil, :path => nil) - - with_scope_level(:exclusive) do - yield - end - ensure - @scope = @scope.parent - end - end - def with_scope_level(kind) @scope = @scope.new_level(kind) yield @@ -1651,16 +1776,11 @@ module ActionDispatch @scope = @scope.parent end - def resource_scope(kind, resource) #:nodoc: - resource.shallow = @scope[:shallow] + def resource_scope(resource) #:nodoc: @scope = @scope.new(:scope_level_resource => resource) - @nesting.push(resource) - with_scope_level(kind) do - scope(parent_resource.resource_scope) { yield } - end + controller(resource.resource_scope) { yield } ensure - @nesting.pop @scope = @scope.parent end @@ -1673,12 +1793,10 @@ module ActionDispatch options end - def nesting_depth #:nodoc: - @nesting.size - end - def shallow_nesting_depth #:nodoc: - @nesting.count(&:shallow?) + @scope.find_all { |node| + node.frame[:scope_level_resource] + }.count { |node| node.frame[:scope_level_resource].shallow? } end def param_constraint? #:nodoc: @@ -1693,27 +1811,28 @@ module ActionDispatch resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope(path, options = {}) #:nodoc: + def shallow_scope #:nodoc: scope = { :as => @scope[:shallow_prefix], :path => @scope[:shallow_path] } @scope = @scope.new scope - scope(path, options) { yield } + yield ensure @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if path.blank? && canonical_action?(action) + return "#{@scope[:path]}/#{path}" if path + + if canonical_action?(action) @scope[:path].to_s else - "#{@scope[:path]}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action)}" end end - def action_path(name, path = nil) #:nodoc: - name = name.to_sym if name.is_a?(String) - path || @scope[:path_names][name] || name.to_s + def action_path(name) #:nodoc: + @scope[:path_names][name.to_sym] || name end def prefix_name_for_action(as, action) #:nodoc: @@ -1765,6 +1884,18 @@ module ActionDispatch delete :destroy if parent_resource.actions.include?(:destroy) end end + + def api_only? + @set.api_only? + end + private + + def path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end end # Routing Concerns allow you to declare common routes that can be reused @@ -1875,14 +2006,14 @@ module ActionDispatch class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] + :shallow, :blocks, :defaults, :via, :format, :options] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] attr_reader :parent, :scope_level - def initialize(hash, parent = {}, scope_level = nil) + def initialize(hash, parent = NULL, scope_level = nil) @hash = hash @parent = parent @scope_level = scope_level @@ -1930,27 +2061,34 @@ module ActionDispatch end def new_level(level) - self.class.new(self, self, level) - end - - def fetch(key, &block) - @hash.fetch(key, &block) + self.class.new(frame, self, level) end def [](key) - @hash.fetch(key) { @parent[key] } + scope = find { |node| node.frame.key? key } + scope && scope.frame[key] end - def []=(k,v) - @hash[k] = v + include Enumerable + + def each + node = self + loop do + break if node.equal? NULL + yield node + node = node.parent + end end + + def frame; @hash; end + + NULL = Scope.new(nil, nil) end def initialize(set) #:nodoc: @set = set @scope = Scope.new({ :path_names => @set.resources_path_names }) @concerns = {} - @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3c1c4fadf6..d6987f4d09 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -24,7 +24,7 @@ module ActionDispatch def serve(req) req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) - + unless uri.host if relative_path?(uri.path) uri.path = "#{req.script_name}/#{uri.path}" @@ -32,7 +32,7 @@ module ActionDispatch uri.path = req.script_name.empty? ? "/" : req.script_name end end - + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -124,7 +124,7 @@ module ActionDispatch url_options[:script_name] = request.script_name end end - + ActionDispatch::Http::URL.url_for url_options end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index d0d8ded515..30e308119d 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -21,8 +21,8 @@ module ActionDispatch alias inspect to_s class Dispatcher < Routing::Endpoint - def initialize(defaults) - @defaults = defaults + def initialize(raise_on_name_error) + @raise_on_name_error = raise_on_name_error @controller_class_names = ThreadSafe::Cache.new end @@ -34,12 +34,11 @@ module ActionDispatch prepare_params!(params) - # Just raise undefined constant errors if a controller was specified as default. - unless controller = controller(params, @defaults.key?(:controller)) + controller = controller(params, @raise_on_name_error) do return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], req.env) + dispatch(controller, params[:action], req) end def prepare_params!(params) @@ -53,24 +52,26 @@ module ActionDispatch # segment, as in :controller(/:action), we should simply return nil and # delegate the control back to Rack cascade. Besides, if this is not a default # controller, it means we should respect the @scope[:module] parameter. - def controller(params, default_controller=true) - if params && params.key?(:controller) - controller_param = params[:controller] - controller_reference(controller_param) - end + def controller(params, raise_on_name_error=true) + controller_reference params.fetch(:controller) { yield } rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if default_controller + raise ActionController::RoutingError, e.message, e.backtrace if raise_on_name_error + yield end - private + protected + + attr_reader :controller_class_names def controller_reference(controller_param) - const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" + const_name = controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" ActiveSupport::Dependencies.constantize(const_name) end - def dispatch(controller, action, env) - controller.action(action).call(env) + private + + def dispatch(controller, action, req) + controller.action(action).call(req.env) end def normalize_controller!(params) @@ -142,6 +143,7 @@ module ActionDispatch end def key?(name) + return unless name routes.key? name.to_sym end @@ -267,9 +269,13 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - path_params -= inner_options.keys - path_params.take(args.size).each do |param| - result[param] = args.shift + inner_options.each_key do |key| + path_params.delete(key) + end + + args.each_with_index do |arg, index| + param = path_params[index] + result[param] = arg if param end end @@ -309,7 +315,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_accessor :default_url_options, :dispatcher_class attr_reader :env_key alias :routes :set @@ -319,17 +325,23 @@ module ActionDispatch end def self.new_with_config(config) + route_set_config = DEFAULT_CONFIG + + # engines apparently don't have this set if config.respond_to? :relative_url_root - new Config.new config.relative_url_root - else - # engines apparently don't have this set - new + route_set_config.relative_url_root = config.relative_url_root end + + if config.respond_to? :api_only + route_set_config.api_only = config.api_only + end + + new route_set_config end - Config = Struct.new :relative_url_root + Config = Struct.new :relative_url_root, :api_only - DEFAULT_CONFIG = Config.new(nil) + DEFAULT_CONFIG = Config.new(nil, false) def initialize(config = DEFAULT_CONFIG) self.named_routes = NamedRouteCollection.new @@ -345,13 +357,18 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set - @formatter = Journey::Formatter.new @set + @formatter = Journey::Formatter.new self + @dispatcher_class = Routing::RouteSet::Dispatcher end def relative_url_root @config.relative_url_root end + def api_only? + @config.api_only + end + def request_class ActionDispatch::Request end @@ -399,8 +416,8 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def dispatcher(defaults) - Routing::RouteSet::Dispatcher.new(defaults) + def dispatcher(raise_on_name_error) + dispatcher_class.new(raise_on_name_error) end module MountedHelpers @@ -498,7 +515,7 @@ module ActionDispatch routes.empty? end - def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + def add_route(mapping, path_ast, name, anchor) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -509,74 +526,17 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = conditions.delete :path_info - ast = conditions.delete :parsed_path_info - path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map(&:to_sym)) - - route = @set.add_route(app, path, conditions, defaults, name) + route = @set.add_route(name, mapping) named_routes[name] = route if name route end - def build_path(path, ast, requirements, anchor) - strexp = Journey::Router::Strexp.new( - ast, - path, - requirements, - SEPARATORS, - anchor) - - pattern = Journey::Path::Pattern.new(strexp) - - builder = Journey::GTG::Builder.new pattern.spec - - # Get all the symbol nodes followed by literals that are not the - # dummy node. - symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| - builder.followpos(n).first.literal? - } - - # Get all the symbol nodes preceded by literals. - symbols.concat pattern.spec.find_all(&:literal?).map { |n| - builder.followpos(n).first - }.find_all(&:symbol?) - - symbols.each { |x| - x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ - } - - pattern - end - private :build_path - - def build_conditions(current_conditions, path_values) - conditions = current_conditions.dup - - # Rack-Mount requires that :request_method be a regular expression. - # :request_method represents the HTTP verb that matches this route. - # - # Here we munge values before they get sent on to rack-mount. - verbs = conditions[:request_method] || [] - unless verbs.empty? - conditions[:request_method] = %r[^#{verbs.join('|')}$] - end - - conditions.keep_if do |k, _| - k == :action || k == :controller || k == :required_defaults || - request_class.public_method_defined?(k) || path_values.include?(k) - end - end - private :build_conditions - class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value - elsif value.is_a?(Array) - value.map(&:to_param).join('/') - elsif param = value.to_param - param + else + value.to_param end end @@ -584,8 +544,8 @@ module ActionDispatch def initialize(named_route, options, recall, set) @named_route = named_route - @options = options.dup - @recall = recall.dup + @options = options + @recall = recall @set = set normalize_recall! @@ -607,7 +567,7 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) if !named_route_exists? || segment_keys.include?(key) - @options[key] = @recall.delete(key) + @options[key] = @recall[key] end end end @@ -661,12 +621,18 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller + if controller + if controller.start_with?("/".freeze) + @options[:controller] = controller[1..-1] + else + @options[:controller] = controller + end + end end # Move 'index' action from options to recall def normalize_action! - if @options[:action] == 'index' + if @options[:action] == 'index'.freeze @recall[:action] = @options.delete(:action) end end @@ -795,12 +761,12 @@ module ActionDispatch if app.matches?(req) && app.dispatcher? dispatcher = app.app - if dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params - else + dispatcher.controller(params, false) do raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" end + + dispatcher.prepare_params!(params) + return params end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index eb554ec383..967bbd62f8 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -52,9 +52,11 @@ module ActionDispatch # argument. # # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' - # in full. However, mailers don't have hostname information, and that's why you'll still - # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and you still have to provide + # the +:host+ argument or set the default host that will be used in all mailers using the + # configuration option +config.action_mailer.default_url_options+. For more information on + # url_for in mailers read the ActionMailer#Base documentation. # # # == URL generation for named routes @@ -147,6 +149,20 @@ module ActionDispatch # # => 'http://somehost.org/myapp/tasks/testing' # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true # # => '/myapp/tasks/testing' + # + # Missing routes keys may be filled in from the current request's parameters + # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are + # placed in the path). Given that the current action has been reached + # through `GET /users/1`: + # + # url_for(only_path: true) # => '/users/1' + # url_for(only_path: true, action: 'edit') # => '/users/1/edit' + # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit' + # + # Notice that no +:id+ parameter was provided to the first +url_for+ call + # and the helper used the one from the route's path. Any path parameter + # implicitly used by +url_for+ can always be overwritten like shown on the + # last +url_for+ calls. def url_for(options = nil) case options when nil @@ -155,6 +171,10 @@ module ActionDispatch route_name = options.delete :use_route _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) + when ActionController::Parameters + route_name = options.delete :use_route + _routes.url_for(options.to_unsafe_h.symbolize_keys. + reverse_merge!(url_options), route_name) when String options when Symbol diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 13a72220b3..b6e21b0d28 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -3,6 +3,13 @@ module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. module ResponseAssertions + RESPONSE_PREDICATES = { # :nodoc: + success: :successful?, + missing: :not_found?, + redirect: :redirection?, + error: :server_error?, + } + # Asserts that the response is one of the following types: # # * <tt>:success</tt> - Status code was in the 200-299 range @@ -20,11 +27,9 @@ module ActionDispatch # # assert that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) - message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>" - if Symbol === type if [:success, :missing, :redirect, :error].include?(type) - assert @response.send("#{type}?"), message + assert_predicate @response, RESPONSE_PREDICATES[type], message else code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] if code.nil? diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index c94eea9134..54e24ed6bf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -86,8 +86,8 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. - generated_path, extra_keys = @routes.generate_extras(options, defaults) - found_extras = options.reject { |k, _| ! extra_keys.include? k } + generated_path, query_string_keys = @routes.generate_extras(options, defaults) + found_extras = options.reject { |k, _| ! query_string_keys.include? k } msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) @@ -165,7 +165,7 @@ module ActionDispatch # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector) + if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector) @controller.send(selector, *args, &block) else super @@ -183,7 +183,7 @@ module ActionDispatch end # Assume given controller - request = ActionController::TestRequest.new + request = ActionController::TestRequest.create if path =~ %r{://} fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index f953429e96..0cdc6d4e77 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -294,7 +294,7 @@ module ActionDispatch if kwarg_request?(args) process(http_method, path, *args) else - non_kwarg_request_warning if args.present? + non_kwarg_request_warning if args.any? process(http_method, path, { params: args[0], headers: args[1] }) end end @@ -325,7 +325,11 @@ module ActionDispatch if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme - host! "#{location.host}:#{location.port}" if location.host + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end path = location.query ? "#{location.path}?#{location.query}" : location.path end @@ -374,7 +378,7 @@ module ActionDispatch @html_document = nil @url_options = nil - @controller = session.last_request.env['action_controller.instance'] + @controller = @request.controller_instance response.status end @@ -391,7 +395,7 @@ module ActionDispatch attr_reader :app - def before_setup + def before_setup # :nodoc: @app = nil @integration_session = nil super @@ -429,7 +433,6 @@ module ActionDispatch # reset the html_document variable, except for cookies/assigns calls unless method == 'cookies' || method == 'assigns' @html_document = nil - reset_template_assertion end integration_session.__send__(method, *args).tap do diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 630e6a9b78..c28d701b48 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -5,9 +5,9 @@ require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess def assigns(key = nil) - assigns = {}.with_indifferent_access - @controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) } - key.nil? ? assigns : assigns[key] + raise NoMethodError, + "assigns has been extracted to a gem. To continue using it, + add `gem 'rails-controller-testing'` to your Gemfile." end def session @@ -19,7 +19,7 @@ module ActionDispatch end def cookies - @request.cookie_jar + @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies) end def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 4b9a088265..ad1a7f7109 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -4,19 +4,22 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request DEFAULT_ENV = Rack::MockRequest.env_for('/', - 'HTTP_HOST' => 'test.host', - 'REMOTE_ADDR' => '0.0.0.0', - 'HTTP_USER_AGENT' => 'Rails Testing' + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing', ) - def self.new(env = {}) - super + # 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 + new(default_env.merge(env)) end - def initialize(env = {}) - env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application - super(default_env.merge(env)) + def self.default_env + DEFAULT_ENV end + private_class_method :default_env def request_method=(method) @env['REQUEST_METHOD'] = method.to_s.upcase @@ -62,17 +65,5 @@ module ActionDispatch @env.delete('action_dispatch.request.accepts') @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") end - - alias :rack_cookies :cookies - - def cookies - @cookies ||= {}.with_indifferent_access - end - - private - - def default_env - DEFAULT_ENV - end end end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index a9b88ac5fd..6a31d6243f 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -16,9 +16,6 @@ module ActionDispatch # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 1690bdd542..60e2cea8a2 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -63,6 +63,10 @@ FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') SharedTestRoutes = ActionDispatch::Routing::RouteSet.new +SharedTestRoutes.draw do + get ':controller(/:action)' +end + module ActionDispatch module SharedRoutes def before_setup @@ -70,35 +74,10 @@ module ActionDispatch super end end - - # Hold off drawing routes until all the possible controller classes - # have been loaded. - module DrawOnce - class << self - attr_accessor :drew - end - self.drew = false - - def before_setup - super - return if DrawOnce.drew - - SharedTestRoutes.draw do - get ':controller(/:action)' - end - - ActionDispatch::IntegrationTest.app.routes.draw do - get ':controller(/:action)' - end - - DrawOnce.drew = true - end - end end module ActiveSupport class TestCase - include ActionDispatch::DrawOnce if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 parallelize_me! end @@ -119,23 +98,25 @@ class RoutedRackApp end class ActionDispatch::IntegrationTest < ActiveSupport::TestCase - include ActionDispatch::SharedRoutes - def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::ParamsParser + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end self.app = build_app + app.routes.draw do + get ':controller(/:action)' + end + # 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 @@ -149,14 +130,10 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end end - def self.stub_controllers - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } - yield ActionDispatch::Routing::RouteSet.new - ensure - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + def self.stub_controllers(config = nil) + route_set = ActionDispatch::Routing::RouteSet.new(*[config].compact) + route_set.dispatcher_class = StubDispatcher + yield route_set end def with_routing(&block) @@ -232,6 +209,10 @@ class Rack::TestCase < ActionDispatch::IntegrationTest end module ActionController + class API + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) + end + class Base # This stub emulates the Railtie including the URL helpers from a Rails application extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 5e64cae7e2..82c747680d 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -7,7 +7,7 @@ module ActionDispatch include ResponseAssertions FakeResponse = Struct.new(:response_code) do - [:success, :missing, :redirect, :error].each do |sym| + [:successful, :not_found, :redirection, :server_error].each do |sym| define_method("#{sym}?") do sym == response_code end @@ -16,7 +16,7 @@ module ActionDispatch def test_assert_response_predicate_methods [:success, :missing, :redirect, :error].each do |sym| - @response = FakeResponse.new sym + @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, '').to_sym assert_response sym assert_raises(Minitest::Assertion) { diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index cbf333c772..beeafc2e53 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -5,9 +5,6 @@ class ActionPackAssertionsController < ActionController::Base def nothing() head :ok end - def hello_world() render :template => "test/hello_world"; end - def hello_repeating_in_path() render :template => "test/hello/hello"; end - def hello_xml_world() render :template => "test/hello_xml_world"; end def hello_xml_world_pdf @@ -20,8 +17,6 @@ class ActionPackAssertionsController < ActionController::Base render :template => "test/hello_xml_world" end - def partial() render :partial => 'test/partial'; end - def redirect_internal() redirect_to "/nothing"; end def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end @@ -48,12 +43,12 @@ class ActionPackAssertionsController < ActionController::Base def flash_me flash['hello'] = 'my name is inigo montoya...' - render :text => "Inconceivable!" + render plain: "Inconceivable!" end def flash_me_naked flash.clear - render :text => "wow!" + render plain: "wow!" end def assign_this @@ -62,40 +57,30 @@ class ActionPackAssertionsController < ActionController::Base end def render_based_on_parameters - render :text => "Mr. #{params[:name]}" + render plain: "Mr. #{params[:name]}" end def render_url - render :text => "<div>#{url_for(:action => 'flash_me', :only_path => true)}</div>" + render html: "<div>#{url_for(action: 'flash_me', only_path: true)}</div>" end def render_text_with_custom_content_type - render :text => "Hello!", :content_type => Mime::RSS - end - - def render_with_layout - @variable_for_layout = nil - render "test/hello_world", :layout => "layouts/standard" - end - - def render_with_layout_and_partial - @variable_for_layout = nil - render "test/hello_world_with_partial", :layout => "layouts/standard" + render body: "Hello!", content_type: Mime::RSS end def session_stuffing session['xmas'] = 'turkey' - render :text => "ho ho ho" + render plain: "ho ho ho" end def raise_exception_on_get raise "get" if request.get? - render :text => "request method: #{request.env['REQUEST_METHOD']}" + render plain: "request method: #{request.env['REQUEST_METHOD']}" end def raise_exception_on_post raise "post" if request.post? - render :text => "request method: #{request.env['REQUEST_METHOD']}" + render plain: "request method: #{request.env['REQUEST_METHOD']}" end def render_file_absolute_path @@ -116,7 +101,7 @@ class AssertResponseWithUnexpectedErrorController < ActionController::Base end def show - render :text => "Boom", :status => 500 + render plain: "Boom", status: 500 end end @@ -304,14 +289,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert session.empty? end - def test_render_template_action - process :nothing - assert_template nil - - process :hello_world - assert_template 'hello_world' - end - def test_redirection_location process :redirect_internal assert_equal 'http://test.host/nothing', @response.redirect_url @@ -341,7 +318,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_missing_response_code process :response404 - assert @response.missing? + assert @response.not_found? end def test_client_error_response_code @@ -369,12 +346,12 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_successful_response_code process :nothing - assert @response.success? + assert @response.successful? end def test_response_object process :nothing - assert_kind_of ActionController::TestResponse, @response + assert_kind_of ActionDispatch::TestResponse, @response end def test_render_based_on_parameters @@ -455,192 +432,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end -class AssertTemplateTest < ActionController::TestCase - tests ActionPackAssertionsController - - def test_with_invalid_hash_keys_raises_argument_error - assert_raise(ArgumentError) do - assert_template foo: "bar" - end - end - - def test_with_partial - get :partial - assert_template :partial => '_partial' - end - - def test_file_with_absolute_path_success - get :render_file_absolute_path - assert_template :file => File.expand_path('../../../README.rdoc', __FILE__) - end - - def test_file_with_relative_path_success - get :render_file_relative_path - assert_template :file => 'README.rdoc' - end - - def test_with_file_failure - get :render_file_absolute_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :file => 'test/hello_world' - end - - get :render_file_absolute_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template file: nil - end - end - - def test_with_nil_passes_when_no_template_rendered - get :nothing - assert_template nil - end - - def test_with_nil_fails_when_template_rendered - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template nil - end - end - - def test_with_empty_string_fails_when_template_rendered - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template "" - end - end - - def test_with_empty_string_fails_when_no_template_rendered - get :nothing - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template "" - end - end - - def test_passes_with_correct_string - get :hello_world - assert_template 'hello_world' - assert_template 'test/hello_world' - end - - def test_passes_with_correct_symbol - get :hello_world - assert_template :hello_world - end - - def test_fails_with_incorrect_string - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'hello_planet' - end - end - - def test_fails_with_incorrect_string_that_matches - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'est/he' - end - end - - def test_fails_with_repeated_name_in_path - get :hello_repeating_in_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'test/hello' - end - end - - def test_fails_with_incorrect_symbol - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :hello_planet - end - end - - def test_fails_with_incorrect_symbol_that_matches - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :"est/he" - end - end - - def test_fails_with_wrong_layout - get :render_with_layout - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :layout => "application" - end - end - - def test_fails_expecting_no_layout - get :render_with_layout - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :layout => nil - end - end - - def test_fails_expecting_not_known_layout - get :render_with_layout - assert_raise(ArgumentError) do - assert_template :layout => 1 - end - end - - def test_passes_with_correct_layout - get :render_with_layout - assert_template :layout => "layouts/standard" - end - - def test_passes_with_layout_and_partial - get :render_with_layout_and_partial - assert_template :layout => "layouts/standard" - end - - def test_passed_with_no_layout - get :hello_world - assert_template :layout => nil - end - - def test_passed_with_no_layout_false - get :hello_world - assert_template :layout => false - end - - def test_passes_with_correct_layout_without_layouts_prefix - get :render_with_layout - assert_template :layout => "standard" - end - - def test_passes_with_correct_layout_symbol - get :render_with_layout - assert_template :layout => :standard - end - - def test_assert_template_reset_between_requests - get :hello_world - assert_template 'test/hello_world' - - get :nothing - assert_template nil - - get :partial - assert_template partial: 'test/_partial' - - get :nothing - assert_template partial: nil - - get :render_with_layout - assert_template layout: 'layouts/standard' - - get :nothing - assert_template layout: nil - - get :render_file_relative_path - assert_template file: 'README.rdoc' - - get :nothing - assert_template file: nil - end -end - class ActionPackHeaderTest < ActionController::TestCase tests ActionPackAssertionsController diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb new file mode 100644 index 0000000000..b4f1673be0 --- /dev/null +++ b/actionpack/test/controller/api/conditional_get_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' +require 'active_support/core_ext/integer/time' +require 'active_support/core_ext/numeric/time' + +class ConditionalGetApiController < ActionController::API + before_action :handle_last_modified_and_etags, only: :two + + def one + if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123]) + render plain: "Hi!" + end + end + + def two + render plain: "Hi!" + end + + private + + def handle_last_modified_and_etags + fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ]) + end +end + +class ConditionalGetApiTest < ActionController::TestCase + tests ConditionalGetApiController + + def setup + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_request_gets_last_modified + get :two + assert_equal @last_modified, @response.headers['Last-Modified'] + assert_response :success + end + + def test_request_obeys_last_modified + @request.if_modified_since = @last_modified + get :two + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :two + assert_response :success + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :one + assert_equal 304, @response.status.to_i + assert @response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] + end +end diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb new file mode 100644 index 0000000000..0e7d97d1f4 --- /dev/null +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +module TestApiFileUtils + def file_path() File.expand_path(__FILE__) end + def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end +end + +class DataStreamingApiController < ActionController::API + include TestApiFileUtils + + def one; end + def two + send_data(file_data, {}) + end +end + +class DataStreamingApiTest < ActionController::TestCase + include TestApiFileUtils + tests DataStreamingApiController + + def test_data + response = process('two') + assert_kind_of String, response.body + assert_equal file_data, response.body + end +end diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb new file mode 100644 index 0000000000..8578340d82 --- /dev/null +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class ForceSSLApiController < ActionController::API + force_ssl + + def one; end + def two + head :ok + end +end + +class ForceSSLApiTest < ActionController::TestCase + tests ForceSSLApiController + + def test_redirects_to_https + get :two + assert_response 301 + assert_equal "https://test.host/force_ssl_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/implicit_render_test.rb b/actionpack/test/controller/api/implicit_render_test.rb new file mode 100644 index 0000000000..26f9cd8f78 --- /dev/null +++ b/actionpack/test/controller/api/implicit_render_test.rb @@ -0,0 +1,15 @@ +require 'abstract_unit' + +class ImplicitRenderAPITestController < ActionController::API + def empty_action + end +end + +class ImplicitRenderAPITest < ActionController::TestCase + tests ImplicitRenderAPITestController + + def test_implicit_no_content_response + get :empty_action + assert_response :no_content + end +end diff --git a/actionpack/test/controller/api/params_wrapper_test.rb b/actionpack/test/controller/api/params_wrapper_test.rb new file mode 100644 index 0000000000..53b3a0c3cc --- /dev/null +++ b/actionpack/test/controller/api/params_wrapper_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +class ParamsWrapperForApiTest < ActionController::TestCase + class UsersController < ActionController::API + attr_accessor :last_parameters + + wrap_parameters :person, format: [:json] + + def test + self.last_parameters = params.except(:controller, :action).to_unsafe_h + head :ok + end + end + + class Person; end + + tests UsersController + + def test_specify_wrapper_name + @request.env['CONTENT_TYPE'] = 'application/json' + post :test, params: { 'username' => 'sikachu' } + + expected = { 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }} + assert_equal expected, @controller.last_parameters + end +end diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb new file mode 100644 index 0000000000..18877c4b3a --- /dev/null +++ b/actionpack/test/controller/api/redirect_to_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' + +class RedirectToApiController < ActionController::API + def one + redirect_to action: "two" + end + + def two; end +end + +class RedirectToApiTest < ActionController::TestCase + tests RedirectToApiController + + def test_redirect_to + get :one + assert_response :redirect + assert_equal "http://test.host/redirect_to_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb new file mode 100644 index 0000000000..9405538833 --- /dev/null +++ b/actionpack/test/controller/api/renderers_test.rb @@ -0,0 +1,38 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class RenderersApiController < ActionController::API + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersApiTest < ActionController::TestCase + tests RenderersApiController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + end + + def test_render_xml + get :two + assert_response :success + assert_equal({ a: 'b' }.to_xml, @response.body) + end +end diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb new file mode 100644 index 0000000000..0d8691a091 --- /dev/null +++ b/actionpack/test/controller/api/url_for_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class UrlForApiController < ActionController::API + def one; end + def two; end +end + +class UrlForApiTest < ActionController::TestCase + tests UrlForApiController + + def setup + super + @request.host = 'www.example.com' + end + + def test_url_for + get :one + assert_equal "http://www.example.com/url_for_api/one", @controller.url_for + end +end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 3240185414..d9374ce9c3 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -53,7 +53,7 @@ end class ActionMissingController < ActionController::Base def action_missing(action) - render :text => "Response for #{action}" + render plain: "Response for #{action}" end end @@ -127,8 +127,6 @@ class PerformActionTest < ActionController::TestCase # a more accurate simulation of what happens in "real life". @controller.logger = ActiveSupport::Logger.new(nil) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 2d6607041d..5698159eba 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -22,8 +22,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = { controller: 'posts', action: 'index' } - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -52,8 +50,6 @@ class FragmentCachingTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = {:controller => 'posts', :action => 'index'} - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -166,6 +162,8 @@ class FunctionalCachingController < CachingController end def formatted_fragment_cached_with_variant + request.variant = :phone if params[:v] == "phone" + respond_to do |format| format.html.phone format.html @@ -183,8 +181,6 @@ class FunctionalFragmentCachingTest < ActionController::TestCase @controller = FunctionalCachingController.new @controller.perform_caching = true @controller.cache_store = @store - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_fragment_caching @@ -268,9 +264,7 @@ CACHED def test_fragment_caching_with_variant - @request.variant = :phone - - get :formatted_fragment_cached_with_variant, format: "html" + get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone } assert_response :success expected_body = "<body>\n<p>PHONE</p>\n</body>\n" @@ -352,6 +346,8 @@ class ViewCacheDependencyTest < ActionController::TestCase end class CollectionCacheController < ActionController::Base + attr_accessor :partial_rendered_times + def index @customers = [Customer.new('david', params[:id] || 1)] end @@ -377,14 +373,17 @@ class AutomaticCollectionCacheTest < ActionController::TestCase super @controller = CollectionCacheController.new @controller.perform_caching = true + @controller.partial_rendered_times = 0 @controller.cache_store = ActiveSupport::Cache::MemoryStore.new + ActionView::PartialRenderer.collection_cache = @controller.cache_store end def test_collection_fetches_cached_views get :index + assert_equal 1, @controller.partial_rendered_times - ActionView::PartialRenderer.expects(:collection_with_template).never get :index + assert_equal 1, @controller.partial_rendered_times end def test_preserves_order_when_reading_from_cache_plus_rendering @@ -402,8 +401,9 @@ class AutomaticCollectionCacheTest < ActionController::TestCase def test_caching_works_with_beginning_comment get :index_with_comment + assert_equal 1, @controller.partial_rendered_times - ActionView::PartialRenderer.expects(:collection_with_template).never get :index_with_comment + assert_equal 1, @controller.partial_rendered_times end end diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index 89667df3a4..c5bbc479c9 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -4,29 +4,29 @@ class OldContentTypeController < ActionController::Base # :ported: def render_content_type_from_body response.content_type = Mime::RSS - render :text => "hello world!" + render body: "hello world!" end # :ported: def render_defaults - render :text => "hello world!" + render body: "hello world!" end # :ported: def render_content_type_from_render - render :text => "hello world!", :content_type => Mime::RSS + render body: "hello world!", :content_type => Mime::RSS end # :ported: def render_charset_from_body response.charset = "utf-16" - render :text => "hello world!" + render body: "hello world!" end # :ported: def render_nil_charset_from_body response.charset = nil - render :text => "hello world!" + render body: "hello world!" end def render_default_for_erb @@ -42,10 +42,10 @@ class OldContentTypeController < ActionController::Base def render_default_content_types_for_respond_to respond_to do |format| - format.html { render :text => "hello world!" } - format.xml { render :action => "render_default_content_types_for_respond_to" } - format.js { render :text => "hello world!" } - format.rss { render :text => "hello world!", :content_type => Mime::XML } + format.html { render body: "hello world!" } + format.xml { render action: "render_default_content_types_for_respond_to" } + format.js { render body: "hello world!" } + format.rss { render body: "hello world!", content_type: Mime::XML } end end end @@ -64,14 +64,14 @@ class ContentTypeTest < ActionController::TestCase def test_render_defaults get :render_defaults assert_equal "utf-8", @response.charset - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::TEXT, @response.content_type end def test_render_changed_charset_default with_default_charset "utf-16" do get :render_defaults assert_equal "utf-16", @response.charset - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::TEXT, @response.content_type end end @@ -92,14 +92,14 @@ class ContentTypeTest < ActionController::TestCase # :ported: def test_charset_from_body get :render_charset_from_body - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::TEXT, @response.content_type assert_equal "utf-16", @response.charset end # :ported: def test_nil_charset_from_body get :render_nil_charset_from_body - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::TEXT, @response.content_type assert_equal "utf-8", @response.charset, @response.headers.inspect end diff --git a/actionpack/test/controller/default_url_options_with_before_action_test.rb b/actionpack/test/controller/default_url_options_with_before_action_test.rb index 230f40d7ad..12fbe0424e 100644 --- a/actionpack/test/controller/default_url_options_with_before_action_test.rb +++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb @@ -6,7 +6,7 @@ class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base after_action { I18n.locale = "en" } def target - render :text => "final response" + render plain: "final response" end def redirect diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index a1ce12a13e..08271012e9 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -13,16 +13,6 @@ class ActionController::Base filters.map!(&:raw_filter) end end - - def assigns(key = nil) - assigns = {} - instance_variables.each do |ivar| - next if ActionController::Base.protected_instance_variables.include?(ivar) - assigns[ivar[1..-1]] = instance_variable_get(ivar) - end - - key.nil? ? assigns : assigns[key.to_s] - end end class FilterTest < ActionController::TestCase @@ -50,7 +40,7 @@ class FilterTest < ActionController::TestCase before_action :ensure_login, :except => [:go_wild] def go_wild - render :text => "gobble" + render plain: "gobble" end end @@ -61,7 +51,7 @@ class FilterTest < ActionController::TestCase (1..3).each do |i| define_method "fail_#{i}" do - render :text => i.to_s + render plain: i.to_s end end @@ -232,7 +222,7 @@ class FilterTest < ActionController::TestCase skip_before_action :clean_up_tmp, only: :login, if: -> { true } def login - render text: 'ok' + render plain: 'ok' end end @@ -244,7 +234,7 @@ class FilterTest < ActionController::TestCase skip_before_action :clean_up_tmp, if: -> { true }, except: :login def login - render text: 'ok' + render plain: 'ok' end end @@ -268,11 +258,11 @@ class FilterTest < ActionController::TestCase before_action :ensure_login, :only => :index def index - render :text => 'ok' + render plain: 'ok' end def public - render :text => 'ok' + render plain: 'ok' end end @@ -282,7 +272,7 @@ class FilterTest < ActionController::TestCase before_action :ensure_login def index - render :text => 'ok' + render plain: 'ok' end private @@ -393,7 +383,7 @@ class FilterTest < ActionController::TestCase before_action(AuditFilter) def show - render :text => "hello" + render plain: "hello" end end @@ -431,11 +421,11 @@ class FilterTest < ActionController::TestCase before_action :second, :only => :foo def foo - render :text => 'foo' + render plain: 'foo' end def bar - render :text => 'bar' + render plain: 'bar' end protected @@ -452,7 +442,7 @@ class FilterTest < ActionController::TestCase before_action :choose %w(foo bar baz).each do |action| - define_method(action) { render :text => action } + define_method(action) { render plain: action } end private @@ -481,7 +471,7 @@ class FilterTest < ActionController::TestCase @ran_filter << 'between_before_all_and_after_all' end def show - render :text => 'hello' + render plain: 'hello' end end @@ -491,7 +481,7 @@ class FilterTest < ActionController::TestCase def around(controller) yield rescue ErrorToRescue => ex - controller.__send__ :render, :text => "I rescued this: #{ex.inspect}" + controller.__send__ :render, plain: "I rescued this: #{ex.inspect}" end end @@ -560,7 +550,7 @@ class FilterTest < ActionController::TestCase def test_after_actions_are_not_run_if_around_action_does_not_yield controller = NonYieldingAroundFilterController.new test_process(controller, "index") - assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] + assert_equal ["filter_one", "it didn't yield"], controller.instance_variable_get(:@filters) end def test_added_action_to_inheritance_graph @@ -577,140 +567,141 @@ class FilterTest < ActionController::TestCase def test_running_actions test_process(PrependingController) - assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"] + assert_equal %w( wonderful_life ensure_login ), + @controller.instance_variable_get(:@ran_filter) end def test_running_actions_with_proc test_process(ProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) end def test_running_actions_with_implicit_proc test_process(ImplicitProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) end def test_running_actions_with_class test_process(AuditController) - assert assigns["was_audited"] + assert @controller.instance_variable_get(:@was_audited) end def test_running_anomolous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) - assert_equal %w( ensure_login ), assigns["ran_filter"] - assert assigns["ran_class_action"] - assert assigns["ran_proc_action1"] - assert assigns["ran_proc_action2"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + assert @controller.instance_variable_get(:@ran_class_action) + assert @controller.instance_variable_get(:@ran_proc_action1) + assert @controller.instance_variable_get(:@ran_proc_action2) test_process(AnomolousYetValidConditionController, "show_without_action") - assert_nil assigns["ran_filter"] - assert !assigns["ran_class_action"] - assert !assigns["ran_proc_action1"] - assert !assigns["ran_proc_action2"] + assert_not @controller.instance_variable_defined?(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_class_action) + assert_not @controller.instance_variable_defined?(:@ran_proc_action1) + assert_not @controller.instance_variable_defined?(:@ran_proc_action2) end def test_running_conditional_options test_process(ConditionalOptionsFilter) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_running_conditional_skip_options test_process(ConditionalOptionsSkipFilter) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_if_is_ignored_when_used_with_only test_process(SkipFilterUsingOnlyAndIf, 'login') - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_except_is_ignored_when_used_with_if test_process(SkipFilterUsingIfAndExcept, 'login') - assert_equal %w(ensure_login), assigns["ran_filter"] + assert_equal %w(ensure_login), @controller.instance_variable_get(:@ran_filter) end def test_skipping_class_actions test_process(ClassController) - assert_equal true, assigns["ran_class_action"] + assert_equal true, @controller.instance_variable_get(:@ran_class_action) skipping_class_controller = Class.new(ClassController) do skip_before_action ConditionalClassFilter end test_process(skipping_class_controller) - assert_nil assigns['ran_class_action'] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_collection_condition_actions test_process(ConditionalCollectionFilterController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalCollectionFilterController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ConditionalCollectionFilterController, "another_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_running_only_condition_actions test_process(OnlyConditionSymController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(OnlyConditionSymController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(OnlyConditionProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) test_process(OnlyConditionProcController, "show_without_action") - assert !assigns["ran_proc_action"] + assert_not @controller.instance_variable_defined?(:@ran_proc_action) test_process(OnlyConditionClassController) - assert assigns["ran_class_action"] + assert @controller.instance_variable_get(:@ran_class_action) test_process(OnlyConditionClassController, "show_without_action") - assert !assigns["ran_class_action"] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_except_condition_actions test_process(ExceptConditionSymController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(ExceptConditionSymController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ExceptConditionProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) test_process(ExceptConditionProcController, "show_without_action") - assert !assigns["ran_proc_action"] + assert_not @controller.instance_variable_defined?(:@ran_proc_action) test_process(ExceptConditionClassController) - assert assigns["ran_class_action"] + assert @controller.instance_variable_get(:@ran_class_action) test_process(ExceptConditionClassController, "show_without_action") - assert !assigns["ran_class_action"] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_only_condition_and_conditional_options test_process(OnlyConditionalOptionsFilter, "show") - assert_not assigns["ran_conditional_index_proc"] + assert_not @controller.instance_variable_defined?(:@ran_conditional_index_proc) end def test_running_before_and_after_condition_actions test_process(BeforeAndAfterConditionController) - assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"] + assert_equal %w( ensure_login clean_up_tmp), @controller.instance_variable_get(:@ran_filter) test_process(BeforeAndAfterConditionController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_around_action test_process(AroundFilterController) - assert assigns["before_ran"] - assert assigns["after_ran"] + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) end def test_before_after_class_action test_process(BeforeAfterClassFilterController) - assert assigns["before_ran"] - assert assigns["after_ran"] + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) end def test_having_properties_in_around_action test_process(AroundFilterController) - assert_equal "before and after", assigns["execution_log"] + assert_equal "before and after", @controller.instance_variable_get(:@execution_log) end def test_prepending_and_appending_around_action @@ -723,33 +714,33 @@ class FilterTest < ActionController::TestCase def test_rendering_breaks_actioning_chain response = test_process(RenderingController) assert_equal "something else", response.body - assert !assigns["ran_action"] + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_rendering_breaks_actioning_chain_for_after_action test_process(RenderingController) - assert_equal %w( before_action_rendering ), assigns["ran_filter"] - assert !assigns["ran_action"] + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_redirects_breaks_actioning_chain_for_after_action test_process(BeforeActionRedirectionController) assert_response :redirect assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url - assert_equal %w( before_action_redirects ), assigns["ran_filter"] + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) end def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action test_process(RenderingForPrependAfterActionController) - assert_equal %w( before_action_rendering ), assigns["ran_filter"] - assert !assigns["ran_action"] + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action test_process(BeforeActionRedirectionForPrependAfterActionController) assert_response :redirect assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url - assert_equal %w( before_action_redirects ), assigns["ran_filter"] + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) end def test_actions_with_mixed_specialization_run_in_order @@ -766,35 +757,34 @@ class FilterTest < ActionController::TestCase def test_dynamic_dispatch %w(foo bar baz).each do |action| - request = ActionController::TestRequest.new - request.query_parameters[:choose] = action - response = DynamicDispatchController.action(action).call(request.env).last + @request.query_parameters[:choose] = action + response = DynamicDispatchController.action(action).call(@request.env).last assert_equal action, response.body end end def test_running_prepended_before_and_after_action test_process(PrependingBeforeAndAfterController) - assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"] + assert_equal %w( before_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter) end def test_skipping_and_limiting_controller test_process(SkippingAndLimitedController, "index") - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(SkippingAndLimitedController, "public") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_skipping_and_reordering_controller test_process(SkippingAndReorderingController, "index") - assert_equal %w( find_record ensure_login ), assigns["ran_filter"] + assert_equal %w( find_record ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_conditional_skipping_of_actions test_process(ConditionalSkippingController, "login") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ConditionalSkippingController, "change_password") - assert_equal %w( ensure_login find_user ), assigns["ran_filter"] + assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalSkippingController, "login") assert !@controller.instance_variable_defined?("@ran_after_action") @@ -804,23 +794,23 @@ class FilterTest < ActionController::TestCase def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(ChildOfConditionalParentController, 'another_action') - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_condition_skipping_of_actions_when_siblings_also_have_conditions test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(AnotherChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) end def test_changing_the_requirements test_process(ChangingTheRequirementsController, "go_wild") - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_a_rescuing_around_action @@ -829,27 +819,25 @@ class FilterTest < ActionController::TestCase response = test_process(RescuedController) end - assert response.success? + assert response.successful? assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end def test_actions_obey_only_and_except_for_implicit_actions test_process(ImplicitActionsController, 'show') - assert_equal 'Except', assigns(:except) - assert_nil assigns(:only) + assert_equal 'Except', @controller.instance_variable_get(:@except) + assert_not @controller.instance_variable_defined?(:@only) assert_equal 'show', response.body test_process(ImplicitActionsController, 'edit') - assert_equal 'Only', assigns(:only) - assert_nil assigns(:except) + assert_equal 'Only', @controller.instance_variable_get(:@only) + assert_not @controller.instance_variable_defined?(:@except) assert_equal 'edit', response.body end private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new process(action) end @@ -1010,8 +998,8 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_with_proc test_process(ControllerWithProcFilter,'no_raise') - assert assigns['before'] - assert assigns['after'] + assert @controller.instance_variable_get(:@before) + assert @controller.instance_variable_get(:@after) end def test_nested_actions @@ -1032,12 +1020,12 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_action_order_with_all_action_types test_process(ControllerWithAllTypesOfFilters,'no_raise') - assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ') + 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') - assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') end def test_first_action_in_multiple_before_action_chain_halts @@ -1063,7 +1051,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_skipping_with_skip_action_callback test_process(SkipFilterUsingSkipActionCallback,'no_raise') - assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') end def test_deprecated_skip_action_callback diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 60a000c160..b063d769a4 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -103,54 +103,55 @@ class FlashTest < ActionController::TestCase get :set_flash get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :use_flash - assert_nil assigns["flash_copy"]["that"], "On second flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" end def test_keep_flash get :set_flash get :use_flash_and_keep_it - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"], "On second flash" + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" get :use_flash - assert_nil assigns["flash_copy"]["that"], "On third flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On third flash" end def test_flash_now get :set_flash_now - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "bar" , assigns["flash_copy"]["foo"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :attempt_to_use_flash_now - assert_nil assigns["flash_copy"]["that"] - assert_nil assigns["flash_copy"]["foo"] - assert_nil assigns["flashy"] + assert_nil @controller.instance_variable_get(:@flash_copy)["that"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] + assert_nil @controller.instance_variable_get(:@flashy) end def test_update_flash get :set_flash get :use_flash_and_update_it - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello again", assigns["flash_copy"]["this"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello again", @controller.instance_variable_get(:@flash_copy)["this"] get :use_flash - assert_nil assigns["flash_copy"]["that"], "On second flash" - assert_equal "hello again", assigns["flash_copy"]["this"], "On second flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" + assert_equal "hello again", + @controller.instance_variable_get(:@flash_copy)["this"], "On second flash" end def test_flash_after_reset_session get :use_flash_after_reset_session - assert_equal "hello", assigns["flashy_that"] - assert_equal "good-bye", assigns["flashy_this"] - assert_nil assigns["flashy_that_reset"] + assert_equal "hello", @controller.instance_variable_get(:@flashy_that) + assert_equal "good-bye", @controller.instance_variable_get(:@flashy_this) + assert_nil @controller.instance_variable_get(:@flashy_that_reset) end def test_does_not_set_the_session_if_the_flash_is_empty @@ -160,13 +161,13 @@ class FlashTest < ActionController::TestCase def test_sweep_after_halted_action_chain get :std_action - assert_nil assigns["flash_copy"]["foo"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] get :filter_halting_action - assert_equal "bar", assigns["flash_copy"]["foo"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] get :std_action # follow redirection - assert_equal "bar", assigns["flash_copy"]["foo"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] get :std_action - assert_nil assigns["flash_copy"]["foo"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] end def test_keep_and_discard_return_values @@ -328,7 +329,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, :key => SessionKey middleware.use ActionDispatch::Flash - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 5639abdc56..22f1cc7c22 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -2,11 +2,11 @@ require 'abstract_unit' class ForceSSLController < ActionController::Base def banana - render :text => "monkey" + render plain: "monkey" end def cheeseburger - render :text => "sikachu" + render plain: "sikachu" end end @@ -26,7 +26,7 @@ class ForceSSLCustomOptions < ForceSSLController force_ssl :notice => 'Foo, Bar!', :only => :redirect_notice def force_ssl_action - render :text => action_name + render plain: action_name end alias_method :redirect_host, :force_ssl_action @@ -40,15 +40,15 @@ class ForceSSLCustomOptions < ForceSSLController alias_method :redirect_notice, :force_ssl_action def use_flash - render :text => flash[:message] + render plain: flash[:message] end def use_alert - render :text => flash[:alert] + render plain: flash[:alert] end def use_notice - render :text => flash[:notice] + render plain: flash[:notice] end end @@ -85,10 +85,10 @@ end class RedirectToSSL < ForceSSLController def banana - force_ssl_redirect || render(:text => 'monkey') + force_ssl_redirect || render(plain: 'monkey') end def cheeseburger - force_ssl_redirect('secure.cheeseburger.host') || render(:text => 'ihaz') + force_ssl_redirect('secure.cheeseburger.host') || render(plain: 'ihaz') end end @@ -240,8 +240,8 @@ class ForceSSLFlashTest < ActionController::TestCase @request.env.delete('PATH_INFO') get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get("@flash_copy")["that"] + assert_equal "hello", @controller.instance_variable_get("@flashy") end end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index e263ed341f..3ecfedefd1 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -73,14 +73,8 @@ module LocalAbcHelper end class HelperPathsTest < ActiveSupport::TestCase - def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - def test_helpers_paths_priority - request = ActionController::TestRequest.new - responses = HelpersPathsController.action(:index).call(request.env) + responses = HelpersPathsController.action(:index).call(ActionController::TestRequest::DEFAULT_ENV.dup) # helpers1_pack was given as a second path, so pack1_helper should be # included as the second one @@ -141,8 +135,7 @@ class HelperTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(ActionController::TestRequest::DEFAULT_ENV.dup) end def test_helper_for_nested_controller @@ -158,7 +151,7 @@ class HelperTest < ActiveSupport::TestCase assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body # # request = ActionController::TestRequest.new - # response = ActionController::TestResponse.new + # response = ActionDispatch::TestResponse.new # request.action = 'test' # # assert_equal 'test: baz', Fun::PdfController.process(request, response).body @@ -249,7 +242,7 @@ class HelperTest < ActiveSupport::TestCase end -class IsolatedHelpersTest < ActiveSupport::TestCase +class IsolatedHelpersTest < ActionController::TestCase class A < ActionController::Base def index render :inline => '<%= shout %>' @@ -273,13 +266,11 @@ class IsolatedHelpersTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(@request.env) end def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + super @request.action = 'index' end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 10fbee7582..0a5e5402b9 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -9,19 +9,19 @@ class HttpBasicAuthenticationTest < ActionController::TestCase http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' if @logged_in end def show - render :text => 'Only for loooooong credentials' + render plain: 'Only for loooooong credentials' end def search - render :text => 'All inline' + render plain: 'All inline' end private @@ -100,6 +100,14 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_no_match(/\n/, result) end + test "succesful authentication with uppercase authorization scheme" do + @request.env['HTTP_AUTHORIZATION'] = "BASIC #{::Base64.encode64("lifo:world")}" + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body, 'Authentication failed when authorization scheme BASIC' + end + test "authentication request without credential" do get :display @@ -122,7 +130,6 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 52a0bc9aa3..f06912bd5a 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -10,11 +10,11 @@ class HttpDigestAuthenticationTest < ActionController::TestCase 'dhh' => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":"))} def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' if @logged_in end private @@ -124,7 +124,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -134,7 +133,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -144,7 +142,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -156,7 +153,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -167,7 +163,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -180,7 +175,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -191,7 +185,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -201,7 +194,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase put :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -244,7 +236,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 2521f0322c..9c5a01c318 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -7,15 +7,15 @@ class HttpTokenAuthenticationTest < ActionController::TestCase before_action :authenticate_long_credentials, only: :show def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' end def show - render :text => 'Only for loooooong credentials' + render plain: 'Only for loooooong credentials' end private @@ -80,13 +80,20 @@ class HttpTokenAuthenticationTest < ActionController::TestCase end test "authentication request with badly formatted header" do - @request.env['HTTP_AUTHORIZATION'] = "Token foobar" + @request.env['HTTP_AUTHORIZATION'] = 'Token token$"lifo"' get :index assert_response :unauthorized assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication header was not properly parsed" end + test "successful authentication request with Bearer instead of Token" do + @request.env['HTTP_AUTHORIZATION'] = 'Bearer lifo' + get :index + + assert_response :success + end + test "authentication request without credential" do get :display diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index a87059bee4..dc4c32b07e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -315,8 +315,6 @@ class IntegrationTestTest < ActiveSupport::TestCase session1 = @test.open_session { |sess| } session2 = @test.open_session # implicit session - assert_respond_to session1, :assert_template, "open_session makes assert_template available" - assert_respond_to session2, :assert_template, "open_session makes assert_template available" assert !session1.equal?(session2) end @@ -361,28 +359,28 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest class IntegrationController < ActionController::Base def get respond_to do |format| - format.html { render :text => "OK", :status => 200 } - format.js { render :text => "JS OK", :status => 200 } + format.html { render plain: "OK", status: 200 } + format.js { render plain: "JS OK", status: 200 } format.xml { render :xml => "<root></root>", :status => 200 } end end def get_with_params - render :text => "foo: #{params[:foo]}", :status => 200 + render plain: "foo: #{params[:foo]}", status: 200 end def post - render :text => "Created", :status => 201 + render plain: "Created", status: 201 end def method - render :text => "method: #{request.method.downcase}" + render plain: "method: #{request.method.downcase}" end def cookie_monster cookies["cookie_1"] = nil cookies["cookie_3"] = "chocolate" - render :text => "Gone", :status => 410 + render plain: "Gone", status: 410 end def set_cookie @@ -391,7 +389,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end def get_cookie - render :text => cookies["foo"] + render plain: cookies["foo"] end def redirect @@ -757,12 +755,31 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest assert_equal "http://test.com/", @request.env["HTTP_REFERER"] end + def test_ignores_common_ports_in_host + get "http://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + + get "https://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + end + + def test_keeps_uncommon_ports_in_host + get "http://test.com:123" + assert_equal "test.com:123", @request.env["HTTP_HOST"] + + get "http://test.com:443" + assert_equal "test.com:443", @request.env["HTTP_HOST"] + + get "https://test.com:80" + assert_equal "test.com:80", @request.env["HTTP_HOST"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def index - render :text => "index" + render plain: "index" end end @@ -849,7 +866,7 @@ end class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def post - render :text => "Created", :status => 201 + render plain: "Created", status: 201 end end @@ -882,15 +899,15 @@ end class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest class FooController < ActionController::Base def index - render :text => "foo#index" + render plain: "foo#index" end def show - render :text => "foo#show" + render plain: "foo#show" end def edit - render :text => "foo#show" + render plain: "foo#show" end end @@ -900,7 +917,7 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest end def index - render :text => "foo#index" + render plain: "foo#index" end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 0c65270ec1..6ba361f2f7 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'active_support/concurrency/latch' +require 'concurrent/atomics' Thread.abort_on_exception = true module ActionController @@ -125,7 +125,7 @@ module ActionController end def render_text - render :text => 'zomg' + render plain: 'zomg' end def default_header @@ -145,7 +145,7 @@ module ActionController response.headers['Content-Type'] = 'text/event-stream' %w{ hello world }.each do |word| response.stream.write word - latch.await + latch.wait end response.stream.close end @@ -162,7 +162,7 @@ module ActionController end def with_stale - render text: 'stale' if stale?(etag: "123", template: false) + render plain: 'stale' if stale?(etag: "123", template: false) end def exception_in_view @@ -212,7 +212,7 @@ module ActionController # .. plus one more, because the #each frees up a slot: response.stream.write '.' - latch.release + latch.count_down # This write will block, and eventually raise response.stream.write 'x' @@ -233,7 +233,7 @@ module ActionController end logger.info 'Work complete' - latch.release + latch.count_down end end @@ -278,7 +278,7 @@ module ActionController def test_async_stream rubinius_skip "https://github.com/rubinius/rubinius/issues/2934" - @controller.latch = ActiveSupport::Concurrency::Latch.new + @controller.latch = Concurrent::CountDownLatch.new parts = ['hello', 'world'] @controller.request = @request @@ -289,8 +289,8 @@ module ActionController resp.stream.each do |part| assert_equal parts.shift, part ol = @controller.latch - @controller.latch = ActiveSupport::Concurrency::Latch.new - ol.release + @controller.latch = Concurrent::CountDownLatch.new + ol.count_down end } @@ -300,23 +300,23 @@ module ActionController end def test_abort_with_full_buffer - @controller.latch = ActiveSupport::Concurrency::Latch.new + @controller.latch = Concurrent::CountDownLatch.new @request.parameters[:format] = 'plain' @controller.request = @request @controller.response = @response - got_error = ActiveSupport::Concurrency::Latch.new + got_error = Concurrent::CountDownLatch.new @response.stream.on_error do ActionController::Base.logger.warn 'Error while streaming' - got_error.release + got_error.count_down end t = Thread.new(@response) { |resp| resp.await_commit _, _, body = resp.to_a body.each do |part| - @controller.latch.await + @controller.latch.wait body.close break end @@ -325,13 +325,13 @@ module ActionController capture_log_output do |output| @controller.process :overfill_buffer_and_die t.join - got_error.await + got_error.wait assert_match 'Error while streaming', output.rewind && output.read end end def test_ignore_client_disconnect - @controller.latch = ActiveSupport::Concurrency::Latch.new + @controller.latch = Concurrent::CountDownLatch.new @controller.request = @request @controller.response = @response @@ -349,7 +349,7 @@ module ActionController @controller.process :ignore_client_disconnect t.join Timeout.timeout(3) do - @controller.latch.await + @controller.latch.wait end assert_match 'Work complete', output.rewind && output.read end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 4ae1b20d43..7835d2768a 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -10,7 +10,7 @@ module Another end rescue_from SpecialException do - head :status => 406 + head 406 end before_action :redirector, only: :never_executed @@ -170,7 +170,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_view_runtime get :show wait - assert_match /Completed 200 OK in [\d]ms/, logs[1] + assert_match(/Completed 200 OK in [\d]ms/, logs[1]) end def test_append_info_to_payload_is_called_even_with_exception diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 7aef8a50ce..64eb33f78f 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -4,43 +4,50 @@ require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout + before_action { + case params[:v] + when String then request.variant = params[:v].to_sym + when Array then request.variant = params[:v].map(&:to_sym) + end + } + def html_xml_or_rss respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } - type.rss { render :text => "RSS" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } end end def js_or_html respond_to do |type| - type.html { render :text => "HTML" } - type.js { render :text => "JS" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.js { render body: "JS" } + type.all { render body: "Nothing" } end end def json_or_yaml respond_to do |type| - type.json { render :text => "JSON" } - type.yaml { render :text => "YAML" } + type.json { render body: "JSON" } + type.yaml { render body: "YAML" } end end def html_or_xml respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.all { render body: "Nothing" } end end def json_xml_or_html respond_to do |type| - type.json { render :text => 'JSON' } + type.json { render body: 'JSON' } type.xml { render :xml => 'XML' } - type.html { render :text => 'HTML' } + type.html { render body: 'HTML' } end end @@ -49,14 +56,14 @@ class RespondToController < ActionController::Base request.format = :xml respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } end end def just_xml respond_to do |type| - type.xml { render :text => "XML" } + type.xml { render body: "XML" } end end @@ -74,52 +81,52 @@ class RespondToController < ActionController::Base def using_defaults_with_all respond_to do |type| type.html - type.all{ render text: "ALL" } + type.all{ render body: "ALL" } end end def made_for_content_type respond_to do |type| - type.rss { render :text => "RSS" } - type.atom { render :text => "ATOM" } - type.all { render :text => "Nothing" } + type.rss { render body: "RSS" } + type.atom { render body: "ATOM" } + type.all { render body: "Nothing" } end end def custom_type_handling respond_to do |type| - type.html { render :text => "HTML" } - type.custom("application/crazy-xml") { render :text => "Crazy XML" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.custom("application/crazy-xml") { render body: "Crazy XML" } + type.all { render body: "Nothing" } end end def custom_constant_handling respond_to do |type| - type.html { render :text => "HTML" } - type.mobile { render :text => "Mobile" } + type.html { render body: "HTML" } + type.mobile { render body: "Mobile" } end end def custom_constant_handling_without_block respond_to do |type| - type.html { render :text => "HTML" } + type.html { render body: "HTML" } type.mobile end end def handle_any respond_to do |type| - type.html { render :text => "HTML" } - type.any(:js, :xml) { render :text => "Either JS or XML" } + type.html { render body: "HTML" } + type.any(:js, :xml) { render body: "Either JS or XML" } end end def handle_any_any respond_to do |type| - type.html { render :text => 'HTML' } - type.any { render :text => 'Whatever you ask for, I got it' } + type.html { render body: 'HTML' } + type.any { render body: 'Whatever you ask for, I got it' } end end @@ -160,15 +167,15 @@ class RespondToController < ActionController::Base request.variant = :mobile respond_to do |type| - type.html { render text: "mobile" } + type.html { render body: "mobile" } end end def multiple_variants_for_format respond_to do |type| type.html do |html| - html.tablet { render text: "tablet" } - html.phone { render text: "phone" } + html.tablet { render body: "tablet" } + html.phone { render body: "phone" } end end end @@ -176,7 +183,7 @@ class RespondToController < ActionController::Base def variant_plus_none_for_format respond_to do |format| format.html do |variant| - variant.phone { render text: "phone" } + variant.phone { render body: "phone" } variant.none end end @@ -184,9 +191,9 @@ class RespondToController < ActionController::Base def variant_inline_syntax respond_to do |format| - format.js { render text: "js" } - format.html.none { render text: "none" } - format.html.phone { render text: "phone" } + format.js { render body: "js" } + format.html.none { render body: "none" } + format.html.phone { render body: "phone" } end end @@ -201,8 +208,8 @@ class RespondToController < ActionController::Base def variant_any respond_to do |format| format.html do |variant| - variant.any(:tablet, :phablet){ render text: "any" } - variant.phone { render text: "phone" } + variant.any(:tablet, :phablet){ render body: "any" } + variant.phone { render body: "phone" } end end end @@ -210,23 +217,23 @@ class RespondToController < ActionController::Base def variant_any_any respond_to do |format| format.html do |variant| - variant.any { render text: "any" } - variant.phone { render text: "phone" } + variant.any { render body: "any" } + variant.phone { render body: "phone" } end end end def variant_inline_any respond_to do |format| - format.html.any(:tablet, :phablet){ render text: "any" } - format.html.phone { render text: "phone" } + format.html.any(:tablet, :phablet){ render body: "any" } + format.html.phone { render body: "phone" } end end def variant_inline_any_any respond_to do |format| - format.html.phone { render text: "phone" } - format.html.any { render text: "any" } + format.html.phone { render body: "phone" } + format.html.any { render body: "any" } end end @@ -239,16 +246,16 @@ class RespondToController < ActionController::Base def variant_any_with_none respond_to do |format| - format.html.any(:none, :phone){ render text: "none or phone" } + format.html.any(:none, :phone){ render body: "none or phone" } end end def format_any_variant_any respond_to do |format| - format.html { render text: "HTML" } + format.html { render body: "HTML" } format.any(:js, :xml) do |variant| - variant.phone{ render text: "phone" } - variant.any(:tablet, :phablet){ render text: "tablet" } + variant.phone{ render body: "phone" } + variant.any(:tablet, :phablet){ render body: "tablet" } end end end @@ -612,8 +619,7 @@ class RespondToControllerTest < ActionController::TestCase logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new old_logger, ActionController::Base.logger = ActionController::Base.logger, logger - @request.variant = :invalid - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :invalid } assert_response :no_content assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" ensure @@ -626,28 +632,24 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_with_implicit_rendering - @request.variant = :implicit - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :implicit } assert_response :no_content end def test_variant_with_implicit_template_rendering - @request.variant = :mobile - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :mobile } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end def test_variant_with_format_and_custom_render - @request.variant = :phone - get :variant_with_format_and_custom_render + get :variant_with_format_and_custom_render, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end def test_multiple_variants_for_format - @request.variant = :tablet - get :multiple_variants_for_format + get :multiple_variants_for_format, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body end @@ -667,32 +669,27 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "none", @response.body - @request.variant = :phone - get :variant_inline_syntax + get :variant_inline_syntax, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_inline_syntax_without_block - @request.variant = :phone - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_any + get :variant_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_any + get :variant_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end @@ -702,54 +699,45 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phone - get :variant_any_any + get :variant_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_any_any + get :variant_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_inline_any + get :variant_inline_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_inline_any + get :variant_inline_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any_any - @request.variant = :phone - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_any_implicit_render - @request.variant = :tablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body - @request.variant = :phablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "phablet", @response.body end @@ -759,37 +747,53 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body - @request.variant = :phone - get :variant_any_with_none + get :variant_any_with_none, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body end def test_format_any_variant_any - @request.variant = :tablet - get :format_any_variant_any, format: :js + get :format_any_variant_any, format: :js, params: { v: :tablet } assert_equal "text/javascript", @response.content_type assert_equal "tablet", @response.body end def test_variant_negotiation_inline_syntax - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_block_syntax - @request.variant = [:tablet, :phone] - get :variant_plus_none_for_format + get :variant_plus_none_for_format, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_without_block - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end end + +class RespondToWithBlockOnDefaultRenderController < ActionController::Base + def show + default_render do + render body: 'default_render yielded' + end + end +end + +class RespondToWithBlockOnDefaultRenderControllerTest < ActionController::TestCase + def setup + super + @request.host = "www.example.com" + end + + def test_default_render_uses_block_when_no_template_exists + get :show + assert_equal "default_render yielded", @response.body + assert_equal "text/plain", @response.content_type + end +end diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index 964f22eb03..0755dafe93 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -6,7 +6,7 @@ module Dispatching before_action :authenticate def index - render :text => "success" + render body: "success" end def modify_response_body @@ -22,7 +22,7 @@ module Dispatching end def show_actions - render :text => "actions: #{action_methods.to_a.sort.join(', ')}" + render body: "actions: #{action_methods.to_a.sort.join(', ')}" end protected @@ -51,7 +51,7 @@ module Dispatching assert_body "success" assert_status 200 - assert_content_type "text/html; charset=utf-8" + assert_content_type "text/plain; charset=utf-8" end # :api: plugin diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb index c8166280fc..c0e92b3b05 100644 --- a/actionpack/test/controller/new_base/content_negotiation_test.rb +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -9,7 +9,7 @@ module ContentNegotiation )] def all - render :text => self.formats.inspect + render plain: self.formats.inspect end end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index 88453988dd..0445a837ca 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -3,16 +3,16 @@ require 'abstract_unit' module ContentType class BaseController < ActionController::Base def index - render :text => "Hello world!" + render body: "Hello world!" end def set_on_response_obj response.content_type = Mime::RSS - render :text => "Hello world!" + render body: "Hello world!" end def set_on_render - render :text => "Hello world!", :content_type => Mime::RSS + render body: "Hello world!", content_type: Mime::RSS end end @@ -30,17 +30,17 @@ module ContentType class CharsetController < ActionController::Base def set_on_response_obj response.charset = "utf-16" - render :text => "Hello world!" + render body: "Hello world!" end def set_as_nil_on_response_obj response.charset = nil - render :text => "Hello world!" + render body: "Hello world!" end end class ExplicitContentTypeTest < Rack::TestCase - test "default response is HTML and UTF8" do + test "default response is text/plain and UTF8" do with_routing do |set| set.draw do get ':controller', :action => 'index' @@ -49,7 +49,7 @@ module ContentType get "/content_type/base" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-8" + assert_header "Content-Type", "text/plain; charset=utf-8" end end @@ -99,14 +99,14 @@ module ContentType get "/content_type/charset/set_on_response_obj" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-16" + assert_header "Content-Type", "text/plain; charset=utf-16" end test "setting the charset of the response as nil directly on the response object" do get "/content_type/charset/set_as_nil_on_response_obj" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-8" + assert_header "Content-Type", "text/plain; charset=utf-8" end end end diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb deleted file mode 100644 index 537b93387a..0000000000 --- a/actionpack/test/controller/new_base/metal_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'abstract_unit' - -module MetalTest - class MetalMiddleware < ActionController::Middleware - def call(env) - if env["PATH_INFO"] =~ /authed/ - app.call(env) - else - [401, headers, "Not authed!"] - end - end - end - - class Endpoint - def call(env) - [200, {}, "Hello World"] - end - end - - class TestMiddleware < ActiveSupport::TestCase - def setup - @app = Rack::Builder.new do - use MetalTest::MetalMiddleware - run MetalTest::Endpoint.new - end.to_app - end - - test "it can call the next app by using @app" do - env = Rack::MockRequest.env_for("/authed") - response = @app.call(env) - - assert_equal ["Hello World"], response[2] - end - - test "it can return a response using the normal AC::Metal techniques" do - env = Rack::MockRequest.env_for("/") - response = @app.call(env) - - assert_equal ["Not authed!"], response[2] - assert_equal 401, response[0] - end - end -end diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index 11a19ab783..963f2c2f5c 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -37,14 +37,14 @@ module Render private def secretz - render :text => "FAIL WHALE!" + render plain: "FAIL WHALE!" end end class DoubleRenderController < ActionController::Base def index - render :text => "hello" - render :text => "world" + render plain: "hello" + render plain: "world" end end diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index 10bad57cd6..435bb18dce 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/deprecation' module RenderText class MinimalController < ActionController::Metal @@ -73,7 +74,10 @@ module RenderText class RenderTextTest < Rack::TestCase test "rendering text from a minimal controller" do - get "/render_text/minimal/index" + ActiveSupport::Deprecation.silence do + get "/render_text/minimal/index" + end + assert_body "Hello World!" assert_status 200 end @@ -82,7 +86,10 @@ module RenderText with_routing do |set| set.draw { get ':controller', action: 'index' } - get "/render_text/simple" + ActiveSupport::Deprecation.silence do + get "/render_text/simple" + end + assert_body "hello david" assert_status 200 end @@ -92,7 +99,9 @@ module RenderText with_routing do |set| set.draw { get ':controller', action: 'index' } - get "/render_text/with_layout" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout" + end assert_body "hello david" assert_status 200 @@ -100,59 +109,81 @@ module RenderText end test "rendering text, while also providing a custom status code" do - get "/render_text/with_layout/custom_code" + 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 - get "/render_text/with_layout/with_nil" + 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 - get "/render_text/with_layout/with_nil_and_status" + 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 - get "/render_text/with_layout/with_false" + 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 - get "/render_text/with_layout/with_layout_true" + 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 - get "/render_text/with_layout/with_custom_layout" + 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 - get "/render_text/with_layout/with_layout_false" + 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 - get "/render_text/with_layout/with_layout_nil" + 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/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb index 744d8664be..6c57c4caeb 100644 --- a/actionpack/test/controller/parameters/mutators_test.rb +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -62,11 +62,15 @@ class ParametersMutatorsTest < ActiveSupport::TestCase end test "select! retains permitted status" do + jruby_skip "https://github.com/jruby/jruby/issues/3137" + @params.permit! assert @params.select! { |k| k != "person" }.permitted? end test "select! retains unpermitted status" do + jruby_skip "https://github.com/jruby/jruby/issues/3137" + assert_not @params.select! { |k| k != "person" }.permitted? end diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb index 3b1257e8d5..7151a8567c 100644 --- a/actionpack/test/controller/parameters/nested_parameters_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_test.rb @@ -136,7 +136,7 @@ class NestedParametersTest < ActiveSupport::TestCase authors_attributes: { :'0' => { name: 'William Shakespeare', age_of_death: '52' }, :'1' => { name: 'Unattributed Assistant' }, - :'2' => { name: %w(injected names)} + :'2' => { name: %w(injected names) } } } }) diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 2ed486516d..9f7d14e85d 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -194,6 +194,19 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "monkey", @params.fetch(:foo) { "monkey" } 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 } + end + + test 'KeyError in fetch block should not be covered up' do + params = ActionController::Parameters.new + e = assert_raises(KeyError) do + params.fetch(:missing_key) { {}.fetch(:also_missing) } + end + assert_match(/:also_missing$/, e.message) + end + test "not permitted is sticky beyond merges" do assert !@params.merge(a: "b").permitted? end @@ -253,7 +266,6 @@ class ParametersPermitTest < ActiveSupport::TestCase assert @params.to_h.is_a? Hash assert_not @params.to_h.is_a? ActionController::Parameters - assert_equal @params.to_hash, @params.to_h end test "to_h returns converted hash when .permit_all_parameters is set" do @@ -284,6 +296,5 @@ class ParametersPermitTest < ActiveSupport::TestCase test "to_unsafe_h returns unfiltered params" do assert @params.to_h.is_a? Hash assert_not @params.to_h.is_a? ActionController::Parameters - assert_equal @params.to_hash, @params.to_unsafe_h end end diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb index 7136fafae5..7c753a45a5 100644 --- a/actionpack/test/controller/permitted_params_test.rb +++ b/actionpack/test/controller/permitted_params_test.rb @@ -2,11 +2,11 @@ require 'abstract_unit' class PeopleController < ActionController::Base def create - render text: params[:person].permitted? ? "permitted" : "forbidden" + render plain: params[:person].permitted? ? "permitted" : "forbidden" end def create_with_permit - render text: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" + render plain: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index ef30f1ea0f..91b30ede6a 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -3,8 +3,8 @@ require 'abstract_unit' 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 :text => 'called status'; end - def location; render :text => 'called location'; end + def status; render plain: 'called status'; end + def location; render plain: 'called location'; end def simple_redirect redirect_to :action => "hello_world" @@ -50,11 +50,6 @@ class RedirectController < ActionController::Base redirect_to :controller => 'module_test/module_redirect', :action => "hello_world" end - def redirect_with_assigns - @hello = "world" - redirect_to :action => "hello_world" - end - def redirect_to_url redirect_to "http://www.rubyonrails.org/" end @@ -215,12 +210,6 @@ class RedirectTest < ActionController::TestCase assert_redirected_to :controller => 'module_test/module_redirect', :action => 'hello_world' end - def test_redirect_with_assigns - get :redirect_with_assigns - assert_response :redirect - assert_equal "world", assigns["hello"] - end - def test_redirect_to_url get :redirect_to_url assert_response :redirect diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index b1ad16bc55..3773900cc4 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -28,7 +28,7 @@ class RenderJsonTest < ActionController::TestCase end def render_json_render_to_string - render :text => render_to_string(:json => '[]') + render plain: render_to_string(json: '[]') end def render_json_hello_world diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index cabacad940..9acdc29aeb 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -10,20 +10,25 @@ class TestControllerWithExtraEtags < ActionController::Base etag { nil } def fresh - render text: "stale" if stale?(etag: '123', template: false) + render plain: "stale" if stale?(etag: '123', template: false) end def array - render text: "stale" if stale?(etag: %w(1 2 3), template: false) + render plain: "stale" if stale?(etag: %w(1 2 3), template: false) end def with_template if stale? template: 'test/hello_world' - render text: 'stale' + render plain: 'stale' end end end +class ImplicitRenderTestController < ActionController::Base + def empty_action + end +end + class TestController < ActionController::Base protect_from_forgery @@ -133,6 +138,14 @@ 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 @@ -146,31 +159,31 @@ class TestController < ActionController::Base end def head_with_location_header - head :location => "/foo" + head :ok, :location => "/foo" end def head_with_location_object - head :location => Customer.new("david", 1) + head :ok, :location => Customer.new("david", 1) end def head_with_symbolic_status - head :status => params[:status].intern + head params[:status].intern end def head_with_integer_status - head :status => params[:status].to_i + head params[:status].to_i end def head_with_string_status - head :status => params[:status] + head params[:status] end def head_with_custom_header - head :x_custom_header => "something" + head :ok, :x_custom_header => "something" end def head_with_www_authenticate_header - head 'WWW-Authenticate' => 'something' + head :ok, 'WWW-Authenticate' => 'something' end def head_with_status_code_first @@ -463,6 +476,15 @@ class MetalRenderTest < ActionController::TestCase end end +class ImplicitRenderTest < ActionController::TestCase + tests ImplicitRenderTestController + + def test_implicit_no_content_response + get :empty_action + assert_response :no_content + end +end + class HeadRenderTest < ActionController::TestCase tests TestController @@ -476,6 +498,19 @@ 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? @@ -587,7 +622,7 @@ class HttpCacheForeverTest < ActionController::TestCase class HttpCacheForeverController < ActionController::Base def cache_me_forever http_cache_forever(public: params[:public], version: params[:version] || 'v1') do - render text: 'hello' + render plain: 'hello' end end end diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index 77a2f68b1c..e5d698d5c2 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -1,11 +1,7 @@ require 'abstract_unit' require 'stringio' -class ActionController::TestRequestTest < ActiveSupport::TestCase - - def setup - @request = ActionController::TestRequest.new - end +class ActionController::TestRequestTest < ActionController::TestCase def test_test_request_has_session_options_initialized assert @request.session_options diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 82c808754c..868520a219 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -13,7 +13,7 @@ module RequestForgeryProtectionActions end def unsafe - render :text => 'pwn' + render plain: 'pwn' end def meta @@ -484,11 +484,10 @@ end class FreeCookieControllerTest < ActionController::TestCase def setup @controller = FreeCookieController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @token = "cf50faa3fe97702ca1ae" SecureRandom.stubs(:base64).returns(@token) + super end def test_should_not_render_form_with_token_tag diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 4898b0c57f..e767323773 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -43,29 +43,29 @@ class RescueController < ActionController::Base rescue_from NotAllowed, :with => proc { head :forbidden } rescue_from 'RescueController::NotAllowedToRescueAsString', :with => proc { head :forbidden } - rescue_from InvalidRequest, :with => proc { |exception| render :text => exception.message } - rescue_from 'InvalidRequestToRescueAsString', :with => proc { |exception| render :text => exception.message } + rescue_from InvalidRequest, with: proc { |exception| render plain: exception.message } + rescue_from 'InvalidRequestToRescueAsString', with: proc { |exception| render plain: exception.message } rescue_from BadGateway do - head :status => 502 + head 502 end rescue_from 'BadGatewayToRescueAsString' do - head :status => 502 + head 502 end rescue_from ResourceUnavailable do |exception| - render :text => exception.message + render plain: exception.message end rescue_from 'ResourceUnavailableToRescueAsString' do |exception| - render :text => exception.message + render plain: exception.message end rescue_from ActionView::TemplateError do - render :text => 'action_view templater error' + render plain: 'action_view templater error' end rescue_from IOError do - render :text => 'io error' + render plain: 'io error' end before_action(only: :before_action_raises) { raise 'umm nice' } @@ -74,7 +74,7 @@ class RescueController < ActionController::Base end def raises - render :text => 'already rendered' + render plain: 'already rendered' raise "don't panic!" end @@ -302,7 +302,7 @@ class RescueTest < ActionDispatch::IntegrationTest rescue_from RecordInvalid, :with => :show_errors def foo - render :text => "foo" + render plain: "foo" end def invalid @@ -315,7 +315,7 @@ class RescueTest < ActionDispatch::IntegrationTest protected def show_errors(exception) - render :text => exception.message + render plain: exception.message end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index f3da2df3ef..dd7c128566 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -505,8 +505,8 @@ class ResourcesTest < ActionController::TestCase routes = @routes.routes routes.each do |route| routes.each do |r| - next if route === r # skip the comparison instance - assert_not_equal [route.conditions, route.path.spec.to_s], [r.conditions, r.path.spec.to_s] + next if route == r # skip the comparison instance + assert_not_equal [route.conditions, route.path.spec.to_s, route.verb], [r.conditions, r.path.spec.to_s, r.verb] end end end @@ -1128,14 +1128,14 @@ class ResourcesTest < ActionController::TestCase end def assert_restful_routes_for(controller_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end new_action = @routes.resources_path_names[:new] || "new" @@ -1154,7 +1154,7 @@ class ResourcesTest < ActionController::TestCase edit_member_path = "#{member_path}/#{edit_action}" formatted_edit_member_path = "#{member_path}/#{edit_action}.xml" - with_options(options[:options]) do |controller| + with_options(route_options) do |controller| controller.assert_routing collection_path, :action => 'index' controller.assert_routing new_path, :action => 'new' controller.assert_routing "#{collection_path}.xml", :action => 'index', :format => 'xml' @@ -1168,23 +1168,23 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_member_path, :action => 'edit', :id => '1', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'index'), :path => collection_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => collection_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'index'), :path => collection_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => collection_path, :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1'), :path => member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1'), :path => edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1'), :path => member_path, :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1'), :path => member_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1', :format => 'xml'), :path => formatted_edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end # test named routes like foo_path and foos_path map to the correct options. @@ -1195,22 +1195,20 @@ class ResourcesTest < ActionController::TestCase end singular_name ||= controller_name.to_s.singularize - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + @controller = "#{route_options[:controller].camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - get :index, params: options[:options] - options[:options].delete :action + get :index, params: route_options + route_options.delete :action path = "#{options[:as] || controller_name}" shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}" @@ -1225,29 +1223,29 @@ class ResourcesTest < ActionController::TestCase edit_action = options[:path_names][:edit] || "edit" end - assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", options[:options] - assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options + assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_routes_for(singleton_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" new_path = "#{full_path}/new" edit_path = "#{full_path}/edit" formatted_edit_path = "#{full_path}/edit.xml" - with_options options[:options] do |controller| + with_options route_options do |controller| controller.assert_routing full_path, :action => 'show' controller.assert_routing new_path, :action => 'new' controller.assert_routing edit_path, :action => 'edit' @@ -1256,42 +1254,41 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_path, :action => 'edit', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'show'), :path => full_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit'), :path => edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => full_path, :method => :post) - assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete) + assert_recognizes(route_options.merge(:action => 'show'), :path => full_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'edit'), :path => edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => full_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'update'), :path => full_path, :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy'), :path => full_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) - assert_recognizes(options[:options].merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) + assert_recognizes(route_options.merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_named_routes_for(singleton_name, options = {}) - (options[:options] ||= {})[:controller] ||= singleton_name.to_s.pluralize - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + route_options = (options[:options] ||= {}).dup + controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize + @controller = "#{controller_name.camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - get :show, params: options[:options] - options[:options].delete :action + get :show, params: route_options + route_options.delete :action full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" name_prefix = options[:name_prefix] - assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') - assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') - assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') + assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') end def assert_named_route(expected, route, options) diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9bbfb74e72..a333290ade 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -335,9 +335,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_colon_first rs.draw do - get '/:controller/:action/:id', :action => 'index', :id => nil - get ':url', :controller => 'tiny_url', :action => 'translate' + get '/:controller/:action/:id', action: 'index', id: nil + get ':url', controller: 'content', action: 'translate' end + + assert_equal({controller: 'content', action: 'translate', url: 'example'}, rs.recognize_path('/example')) end def test_route_with_regexp_for_controller diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 36c57ec9b2..c0ddcf7f50 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -34,8 +34,6 @@ class SendFileTest < ActionController::TestCase def setup @controller = SendFileController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_file_nostream diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index fbfc891d19..1c5de983d8 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -6,78 +6,78 @@ require 'rails/engine' class TestCaseTest < ActionController::TestCase class TestController < ActionController::Base def no_op - render text: 'dummy' + render plain: 'dummy' end def set_flash flash["test"] = ">#{flash["test"]}<" - render text: 'ignore me' + render plain: 'ignore me' end def delete_flash flash.delete("test") - render :text => 'ignore me' + render plain: 'ignore me' end def set_flash_now flash.now["test_now"] = ">#{flash["test_now"]}<" - render text: 'ignore me' + render plain: 'ignore me' end def set_session session['string'] = 'A wonder' session[:symbol] = 'it works' - render text: 'Success' + render plain: 'Success' end def reset_the_session reset_session - render text: 'ignore me' + render plain: 'ignore me' end def render_raw_post raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank? - render text: request.raw_post + render plain: request.raw_post end def render_body - render text: request.body.read + render plain: request.body.read end def test_params - render text: params.inspect + render plain: ::JSON.dump(params.to_unsafe_h) end def test_query_parameters - render text: request.query_parameters.inspect + render plain: ::JSON.dump(request.query_parameters) end def test_request_parameters - render text: request.request_parameters.inspect + render plain: request.request_parameters.inspect end def test_uri - render text: request.fullpath + render plain: request.fullpath end def test_format - render text: request.format + render plain: request.format end def test_query_string - render text: request.query_string + render plain: request.query_string end def test_protocol - render text: request.protocol + render plain: request.protocol end def test_headers - render text: request.headers.env.to_json + render plain: request.headers.env.to_json end def test_html_output - render text: <<HTML + render plain: <<HTML <html> <body> <a href="/"><img src="/images/button.png" /></a> @@ -99,7 +99,7 @@ HTML def test_xml_output response.content_type = "application/xml" - render text: <<XML + 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> @@ -108,15 +108,15 @@ XML end def test_only_one_param - render text: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" + render plain: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" end def test_remote_addr - render text: (request.remote_addr || "not specified") + render plain: (request.remote_addr || "not specified") end def test_file_upload - render text: params[:file].size + render plain: params[:file].size end def test_send_file @@ -140,12 +140,6 @@ XML head :ok end - def test_assigns - @foo = "foo" - @foo_hash = { foo: :bar } - head :ok - end - def test_without_body render html: '<div class="foo"></div>'.html_safe end @@ -164,8 +158,6 @@ XML def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do @@ -174,22 +166,11 @@ XML end end - class ViewAssignsController < ActionController::Base - def test_assigns - @foo = "foo" - head :ok - end - - def view_assigns - { "bar" => "bar" } - end - end - class DefaultUrlOptionsCachingController < ActionController::Base before_action { @dynamic_opt = 'opt' } def test_url_options_reset - render text: url_for(params) + render plain: url_for(params) end def default_url_options @@ -246,7 +227,7 @@ XML 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"}), @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 @@ -446,30 +427,6 @@ XML assert_equal "OK", @response.body end - def test_assigns - process :test_assigns - # assigns can be accessed using assigns(key) - # or assigns[key], where key is a string or - # a symbol - assert_equal "foo", assigns(:foo) - assert_equal "foo", assigns("foo") - assert_equal "foo", assigns[:foo] - assert_equal "foo", assigns["foo"] - - # but the assigned variable should not have its own keys stringified - expected_hash = { foo: :bar } - assert_equal expected_hash, assigns(:foo_hash) - end - - def test_view_assigns - @controller = ViewAssignsController.new - process :test_assigns - assert_equal nil, assigns(:foo) - assert_equal nil, assigns[:foo] - assert_equal "bar", assigns(:bar) - assert_equal "bar", assigns[:bar] - end - def test_should_not_impose_childless_html_tags_in_xml process :test_xml_output @@ -526,7 +483,7 @@ XML assert_deprecated { get :test_params, page: { name: "Page name", month: '4', year: '2004', day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -545,7 +502,7 @@ XML day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -557,8 +514,8 @@ XML def test_query_param_named_action get :test_query_parameters, params: {action: 'foobar'} - parsed_params = eval(@response.body) - assert_equal({action: 'foobar'}, parsed_params) + parsed_params = JSON.parse(@response.body) + assert_equal({'action' => 'foobar'}, parsed_params) end def test_request_param_named_action @@ -577,7 +534,7 @@ XML } }, session: { 'foo' => 'bar' }, flash: { notice: 'created' } - parsed_params = eval(@response.body) + 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'}}, @@ -592,7 +549,7 @@ XML get :test_params, params: { page: { name: "Page name", month: 4, year: 2004, day: 6 } } - parsed_params = eval(@response.body) + 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'}}, @@ -602,17 +559,17 @@ XML def test_params_passing_with_fixnums_when_not_html_request get :test_params, params: { format: 'json', count: 999 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', - 'format' => 'json', 'count' => 999 }, + 'format' => 'json', 'count' => '999' }, parsed_params ) end def test_params_passing_path_parameter_is_string_when_not_html_request get :test_params, params: { format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -622,7 +579,7 @@ XML def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request assert_deprecated { get :test_params, format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -636,7 +593,7 @@ XML frozen: 'icy'.freeze, frozens: ['icy'.freeze].freeze, deepfreeze: { frozen: 'icy'.freeze }.freeze } end - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'frozen' => 'icy', 'frozens' => ['icy'], 'deepfreeze' => { 'frozen' => 'icy' }}, @@ -734,13 +691,13 @@ XML 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"}), @response.body) + 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"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_xhr_with_session @@ -761,12 +718,6 @@ XML assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access" end - def test_header_properly_reset_after_get_request - get :test_params - @request.recycle! - assert_nil @request.instance_variable_get("@request_method") - end - def test_deprecated_params_reset_between_post_requests assert_deprecated { post :no_op, foo: "bar" } assert_equal "bar", @request.params[:foo] @@ -957,7 +908,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" assert_deprecated { - post :test_file_upload, file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + post :test_file_upload, file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -966,7 +917,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" post :test_file_upload, params: { - file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -998,10 +949,11 @@ class ResponseDefaultHeadersTest < ActionController::TestCase end end - setup do + def before_setup @original = ActionDispatch::Response.default_headers @defaults = { 'A' => '1', 'B' => '2' } ActionDispatch::Response.default_headers = @defaults + super end teardown do @@ -1011,8 +963,6 @@ class ResponseDefaultHeadersTest < ActionController::TestCase def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do @@ -1047,7 +997,7 @@ module EngineControllerTests class BarController < ActionController::Base def index - render text: 'bar' + render plain: 'bar' end end @@ -1133,7 +1083,7 @@ class AnonymousControllerTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def index - render text: params[:controller] + render plain: params[:controller] end end.new @@ -1154,11 +1104,11 @@ class RoutingDefaultsTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def post - render text: request.fullpath + render plain: request.fullpath end def project - render text: request.fullpath + render plain: request.fullpath end end.new diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index d9a1ae7d4f..5f2abc9606 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'controller/fake_controllers' -class UrlRewriterTests < ActiveSupport::TestCase +class UrlRewriterTests < ActionController::TestCase class Rewriter def initialize(request) @options = { @@ -16,7 +16,6 @@ class UrlRewriterTests < ActiveSupport::TestCase end def setup - @request = ActionController::TestRequest.new @params = {} @rewriter = Rewriter.new(@request) #.new(@request, @params) @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 21fa670bb6..b26f037c36 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -5,16 +5,22 @@ class WebServiceTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def assign_parameters if params[:full] - render :text => dump_params_keys + render plain: dump_params_keys else - render :text => (params.keys - ['controller', 'action']).sort.join(", ") + render plain: (params.keys - ['controller', 'action']).sort.join(", ") end end def dump_params_keys(hash = params) hash.keys.sort.inject("") do |s, k| value = hash[k] - value = Hash === value ? "(#{dump_params_keys(value)})" : "" + + if value.is_a?(Hash) || value.is_a?(ActionController::Parameters) + value = "(#{dump_params_keys(value)})" + else + value = "" + end + s << ", " unless s.empty? s << "#{k}#{value}" end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 6223a52a76..aca28ae8d1 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -280,7 +280,7 @@ class CookiesTest < ActionController::TestCase def test_setting_the_same_value_to_cookie request.cookies[:user_name] = 'david' get :authenticate - assert response.cookies.empty? + assert_predicate response.cookies, :empty? end def test_setting_the_same_value_to_permanent_cookie @@ -360,7 +360,7 @@ class CookiesTest < ActionController::TestCase def test_delete_unexisting_cookie request.cookies.clear get :delete_cookie - assert @response.cookies.empty? + assert_predicate @response.cookies, :empty? end def test_deleted_cookie_predicate @@ -378,7 +378,7 @@ class CookiesTest < ActionController::TestCase def test_cookies_persist_throughout_request response = get :authenticate - assert response.headers["Set-Cookie"] =~ /user_name=david/ + assert_match(/user_name=david/, response.headers["Set-Cookie"]) end def test_set_permanent_cookie diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index 7a29a7ff97..3dac582407 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -19,15 +19,13 @@ module ActionDispatch setup do Rails.stubs(:root).returns(Pathname.new('.')) - cleaner = ActiveSupport::BacktraceCleaner.new - cleaner.add_silencer { |line| line !~ /^lib/ } - - @environment = { 'action_dispatch.backtrace_cleaner' => cleaner } + @cleaner = ActiveSupport::BacktraceCleaner.new + @cleaner.add_silencer { |line| line !~ /^lib/ } end test '#source_extracts fetches source fragments for every backtrace entry' do exception = TestError.new("lib/file.rb:42:in `index'") - wrapper = ExceptionWrapper.new({}, exception) + wrapper = ExceptionWrapper.new(nil, exception) wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo') @@ -37,7 +35,7 @@ module ActionDispatch test '#source_extracts works with Windows paths' do exc = TestError.new("c:/path/to/rails/app/controller.rb:27:in 'index':") - wrapper = ExceptionWrapper.new({}, exc) + wrapper = ExceptionWrapper.new(nil, exc) wrapper.expects(:source_fragment).with('c:/path/to/rails/app/controller.rb', 27).returns('nothing') assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts @@ -46,7 +44,7 @@ module ActionDispatch test '#source_extracts works with non standard backtrace' do exc = TestError.new('invalid') - wrapper = ExceptionWrapper.new({}, exc) + wrapper = ExceptionWrapper.new(nil, exc) wrapper.expects(:source_fragment).with('invalid', 0).returns('nothing') assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts @@ -54,14 +52,14 @@ module ActionDispatch test '#application_trace returns traces only from the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace end test '#application_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.application_trace assert_equal [], nil_cleaner_wrapper.application_trace @@ -69,14 +67,14 @@ module ActionDispatch test '#framework_trace returns traces outside the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal caller, wrapper.framework_trace end test '#framework_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.framework_trace assert_equal [], nil_cleaner_wrapper.framework_trace @@ -84,14 +82,14 @@ module ActionDispatch test '#full_trace returns application and framework traces' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal exception.backtrace, wrapper.full_trace end test '#full_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.full_trace assert_equal [], nil_cleaner_wrapper.full_trace @@ -99,7 +97,7 @@ module ActionDispatch test '#traces returns every trace by category enumerated with an index' do exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'") - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal({ 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ], diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index 512f3a8a7a..5cfa5f7b3b 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'active_support/concurrency/latch' +require 'concurrent/atomics' module ActionController module Live @@ -27,18 +27,18 @@ module ActionController end def test_parallel - latch = ActiveSupport::Concurrency::Latch.new + latch = Concurrent::CountDownLatch.new t = Thread.new { @response.stream.write 'foo' - latch.await + latch.wait @response.stream.close } @response.await_commit @response.each do |part| assert_equal 'foo', part - latch.release + latch.count_down end assert t.join end @@ -62,15 +62,15 @@ module ActionController def test_headers_cannot_be_written_after_webserver_reads @response.stream.write 'omg' - latch = ActiveSupport::Concurrency::Latch.new + latch = Concurrent::CountDownLatch.new t = Thread.new { @response.stream.each do |chunk| - latch.release + latch.count_down end } - latch.await + latch.wait assert @response.headers.frozen? e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers['Content-Length'] = "zomg" diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 889f9a4736..f35ffd8845 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -4,13 +4,6 @@ module ActionDispatch module Routing class MapperTest < ActiveSupport::TestCase class FakeSet < ActionDispatch::Routing::RouteSet - attr_reader :routes - alias :set :routes - - def initialize - @routes = [] - end - def resources_path_names {} end @@ -19,16 +12,24 @@ module ActionDispatch ActionDispatch::Request end - def add_route(*args) - routes << args + def dispatcher_class + RouteSet::Dispatcher + end + + def defaults + routes.map(&:defaults) end def conditions - routes.map { |x| x[1] } + routes.map(&:constraints) end def requirements - routes.map { |x| x[2] } + routes.map(&:path).map(&:requirements) + end + + def asts + routes.map(&:path).map(&:spec) end end @@ -36,18 +37,76 @@ module ActionDispatch Mapper.new FakeSet.new end + def test_scope_raises_on_anchor + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.scope(anchor: false) do + end + end + end + + def test_blows_up_without_via + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.match '/', :to => 'posts#index', :as => :main + end + end + + def test_unscoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get '/foo', :to => 'posts#index', :as => :main, :format => true + assert_equal({:controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_scoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(format: true) do + mapper.get '/foo', :to => 'posts#index', :as => :main + end + assert_equal({:controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_random_keys + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(omg: :awesome) do + mapper.get '/', :to => 'posts#index', :as => :main + end + assert_equal({:omg=>:awesome, :controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal(/^GET$/, fakeset.routes.first.verb) + end + def test_mapping_requirements - options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options) - _, _, requirements, _ = m.to_route - assert_equal(/.+?/, requirements[:rest]) + options = { } + scope = Mapper::Scope.new({}) + ast = Journey::Parser.parse '/store/:name(*rest)' + m = Mapper::Mapping.build(scope, FakeSet.new, ast, 'foo', 'bar', nil, [:get], nil, {}, true, options) + assert_equal(/.+?/, m.requirements[:rest]) + end + + def test_via_scope + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(via: :put) do + mapper.match '/', :to => 'posts#index', :as => :main + end + assert_equal(/^PUT$/, fakeset.routes.first.verb) end def test_map_slash fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/', :to => 'posts#index', :as => :main - assert_equal '/', fakeset.conditions.first[:path_info] + assert_equal '/', fakeset.asts.first.to_s end def test_map_more_slashes @@ -56,14 +115,14 @@ module ActionDispatch # FIXME: is this a desired behavior? mapper.get '/one/two/', :to => 'posts#index', :as => :main - assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/one/two(.:format)', fakeset.asts.first.to_s end def test_map_wildcard fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show' - assert_equal '/*path(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*path(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:path]) end @@ -71,7 +130,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path/foo/:bar', :to => 'pages#show' - assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*path/foo/:bar(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:path]) end @@ -79,7 +138,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*foo/*bar', :to => 'pages#show' - assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*foo/*bar(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:foo]) assert_equal(/.+?/, fakeset.requirements.first[:bar]) end @@ -88,7 +147,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show', :format => false - assert_equal '/*path', fakeset.conditions.first[:path_info] + assert_equal '/*path', fakeset.asts.first.to_s assert_nil fakeset.requirements.first[:path] end @@ -96,7 +155,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show', :format => true - assert_equal '/*path.:format', fakeset.conditions.first[:path_info] + assert_equal '/*path.:format', fakeset.asts.first.to_s end def test_raising_helpful_error_on_invalid_arguments diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb deleted file mode 100644 index 9607f026db..0000000000 --- a/actionpack/test/dispatch/middleware_stack/middleware_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'abstract_unit' -require 'action_dispatch/middleware/stack' - -module ActionDispatch - class MiddlewareStack - class MiddlewareTest < ActiveSupport::TestCase - class Omg; end - - { - 'concrete' => Omg, - 'anonymous' => Class.new - }.each do |name, klass| - - define_method("test_#{name}_klass") do - mw = Middleware.new klass - assert_equal klass, mw.klass - end - - define_method("test_#{name}_==") do - mw1 = Middleware.new klass - mw2 = Middleware.new klass - assert_equal mw1, mw2 - end - - end - - def test_string_class - mw = Middleware.new Omg.name - assert_equal Omg, mw.klass - end - - def test_double_equal_works_with_classes - k = Class.new - mw = Middleware.new k - assert_operator mw, :==, k - - result = mw != Class.new - assert result, 'middleware should not equal other anon class' - end - - def test_double_equal_works_with_strings - mw = Middleware.new Omg - assert_operator mw, :==, Omg.name - end - - def test_double_equal_normalizes_strings - mw = Middleware.new Omg - assert_operator mw, :==, "::#{Omg.name}" - end - - def test_middleware_loads_classnames_from_cache - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg.name) - - fake_cache = { mw.name => Omg } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - - fake_cache[mw.name] = Middleware - assert_equal Middleware, mw.klass - end - - def test_middleware_always_returns_class - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg) - - fake_cache = { mw.name => Middleware } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - end - end - end -end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 948a690979..33aa616474 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -4,6 +4,7 @@ class MiddlewareStackTest < ActiveSupport::TestCase class FooMiddleware; end class BarMiddleware; end class BazMiddleware; end + class HiyaMiddleware; end class BlockMiddleware attr_reader :block def initialize(&block) @@ -17,6 +18,20 @@ 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 + end + end + test "use should push middleware as class onto the stack" do assert_difference "@stack.size" do @stack.use BazMiddleware @@ -25,17 +40,21 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "use should push middleware as a string onto the stack" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware as a symbol onto the stack" do - assert_difference "@stack.size" do - @stack.use :"MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use :"MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware class with arguments onto the stack" do @@ -88,30 +107,28 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "unshift adds a new middleware at the beginning of the stack" do - @stack.unshift :"MiddlewareStackTest::BazMiddleware" - assert_equal BazMiddleware, @stack.first.klass + assert_deprecated do + @stack.unshift :"MiddlewareStackTest::BazMiddleware" + assert_equal BazMiddleware, @stack.first.klass + end end test "raise an error on invalid index" do assert_raise RuntimeError do - @stack.insert("HiyaMiddleware", BazMiddleware) + @stack.insert(HiyaMiddleware, BazMiddleware) end assert_raise RuntimeError do - @stack.insert_after("HiyaMiddleware", BazMiddleware) + @stack.insert_after(HiyaMiddleware, BazMiddleware) end end test "lazy evaluates middleware class" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass - end - - test "lazy compares so unloaded constants are not loaded" do - @stack.use "UnknownMiddleware" - @stack.use :"MiddlewareStackTest::BazMiddleware" - assert @stack.include?("::MiddlewareStackTest::BazMiddleware") end end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index f90d5499d7..d75e31db62 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -73,26 +73,26 @@ module TestGenerationPrefix include RailsApplication.routes.mounted_helpers def index - render :text => posts_path + render plain: posts_path end def show - render :text => post_path(:id => params[:id]) + render plain: post_path(id: params[:id]) end def url_to_application path = main_app.url_for(:controller => "outside_engine_generating", :action => "index", :only_path => true) - render :text => path + render plain: path end def polymorphic_path_for_engine - render :text => polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def conflicting - render :text => "engine" + render plain: "engine" end end @@ -101,28 +101,28 @@ module TestGenerationPrefix include RailsApplication.routes.url_helpers def index - render :text => blog_engine.post_path(:id => 1) + render plain: blog_engine.post_path(id: 1) end def polymorphic_path_for_engine - render :text => blog_engine.polymorphic_path(Post.new) + render plain: blog_engine.polymorphic_path(Post.new) end def polymorphic_path_for_app - render :text => polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def polymorphic_with_url_for - render :text => blog_engine.url_for(Post.new) + render plain: blog_engine.url_for(Post.new) end def conflicting - render :text => "application" + render plain: "application" end def ivar_usage @blog_engine = "Not the engine route helper" - render :text => blog_engine.post_path(:id => 1) + render plain: blog_engine.post_path(id: 1) end end @@ -378,7 +378,7 @@ module TestGenerationPrefix include RailsApplication.routes.mounted_helpers def show - render :text => post_path(:id => params[:id]) + render plain: post_path(id: params[:id]) 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 d77341bc64..c2300a0142 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -113,7 +113,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest def parse self.class.last_request_parameters = request.request_parameters - self.class.last_parameters = params + self.class.last_parameters = params.to_unsafe_h head :ok end end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 50f69c53cb..b36fbd3c76 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -17,7 +17,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end def read - render :text => "File: #{params[:uploaded_data].read}" + render plain: "File: #{params[:uploaded_data].read}" end end @@ -63,6 +63,17 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal 'contents', file.read end + test "parses utf8 filename with percent character" do + params = parse_multipart('utf8_filename') + assert_equal %w(file foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_equal 'ファイル%名.txt', file.original_filename + assert_equal "text/plain", file.content_type + assert_equal 'contents', file.read + end + test "parses boundary problem file" do params = parse_multipart('boundary_problem_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 27ee8603e4..ff63c10e8d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -989,6 +989,7 @@ class RequestParameterFilter < BaseRequestTest [{'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| diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 5fbd19acdf..780e7dc3e2 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -42,6 +42,13 @@ class ResponseTest < ActiveSupport::TestCase assert_equal Encoding::UTF_8, response.body.encoding end + def test_response_charset_writer + @response.charset = 'utf-16' + assert_equal 'utf-16', @response.charset + @response.charset = nil + assert_equal 'utf-8', @response.charset + end + test "simple output" do @response.body = "Hello, World!" @@ -72,12 +79,14 @@ class ResponseTest < ActiveSupport::TestCase test "content type" do [204, 304].each do |c| + @response = ActionDispatch::Response.new @response.status = c.to_s _, headers, _ = @response.to_a assert !headers.has_key?("Content-Type"), "#{c} should not have Content-Type header" end [200, 302, 404, 500].each do |c| + @response = ActionDispatch::Response.new @response.status = c.to_s _, headers, _ = @response.to_a assert headers.has_key?("Content-Type"), "#{c} did not have Content-Type header" @@ -169,6 +178,8 @@ class ResponseTest < ActiveSupport::TestCase end test "read content type without charset" do + jruby_skip "https://github.com/jruby/jruby/issues/3138" + original = ActionDispatch::Response.default_charset begin ActionDispatch::Response.default_charset = 'utf-16' diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb index 7ef513b0c8..361ceca677 100644 --- a/actionpack/test/dispatch/routing/concerns_test.rb +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -109,6 +109,8 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest end def test_concerns_executes_block_in_context_of_current_mapper + jruby_skip "https://github.com/jruby/jruby/issues/3143" + mapper = ActionDispatch::Routing::Mapper.new(ActionDispatch::Routing::RouteSet.new) mapper.concern :test_concern do resources :things diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 62c99a2edc..332a550de0 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -167,6 +167,44 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/session/reset', reset_session_path end + def test_session_singleton_resource_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resource :session do + get :create + post :reset + end + end + @app = RoutedRackApp.new routes + end + + get '/session' + assert_equal 'sessions#create', @response.body + assert_equal '/session', session_path + + post '/session' + assert_equal 'sessions#create', @response.body + + put '/session' + assert_equal 'sessions#update', @response.body + + delete '/session' + assert_equal 'sessions#destroy', @response.body + + post '/session/reset' + assert_equal 'sessions#reset', @response.body + assert_equal '/session/reset', reset_session_path + + get '/session/new' + assert_equal 'Not Found', @response.body + + get '/session/edit' + assert_equal 'Not Found', @response.body + end + def test_session_info_nested_singleton_resource draw do resource :session do @@ -323,9 +361,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_pagemarks + tc = self draw do scope "pagemark", :controller => "pagemarks", :as => :pagemark do - get "new", :path => "build" + tc.assert_deprecated do + get "new", :path => "build" + end post "create", :as => "" put "update" get "remove", :action => :destroy, :as => :remove @@ -509,6 +550,40 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resources :projects, controller: :project + end + @app = RoutedRackApp.new routes + end + + get '/projects' + assert_equal 'project#index', @response.body + assert_equal '/projects', projects_path + + post '/projects' + assert_equal 'project#create', @response.body + + get '/projects.xml' + assert_equal 'project#index', @response.body + assert_equal '/projects.xml', projects_path(format: 'xml') + + get '/projects/1' + assert_equal 'project#show', @response.body + assert_equal '/projects/1', project_path(id: '1') + + get '/projects/1.xml' + assert_equal 'project#show', @response.body + assert_equal '/projects/1.xml', project_path(id: '1', format: 'xml') + + get '/projects/1/edit' + assert_equal 'Not Found', @response.body + end + def test_projects_with_post_action_and_new_path_on_collection draw do resources :projects, :controller => :project do @@ -3492,12 +3567,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/' end - # TODO: HEAD request should match `get /home` rather than the + # HEAD request should match `get /home` rather than the # lower-precedence Rack app mounted at `/`. head '/home' assert_response :ok - #assert_equal 'test#index', @response.body - assert_equal 'HEAD', @response.body + assert_equal 'test#index', @response.body # But the Rack app can still respond to its own HEAD requests. head '/foobar' @@ -3547,7 +3621,7 @@ private end class TestAltApp < ActionDispatch::IntegrationTest - class AltRequest + class AltRequest < ActionDispatch::Request attr_accessor :path_parameters, :path_info, :script_name attr_reader :env @@ -3556,6 +3630,7 @@ class TestAltApp < ActionDispatch::IntegrationTest @env = env @path_info = "/" @script_name = "" + super end def request_method @@ -3655,7 +3730,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest module ::Admin class StorageFilesController < ActionController::Base def index - render :text => "admin/storage_files#index" + render plain: "admin/storage_files#index" end end end @@ -3750,7 +3825,7 @@ class TestDefaultScope < ActionDispatch::IntegrationTest module ::Blog class PostsController < ActionController::Base def index - render :text => "blog/posts#index" + render plain: "blog/posts#index" end end end @@ -4090,13 +4165,13 @@ end class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest class CategoriesController < ActionController::Base def show - render :text => "categories#show" + render plain: "categories#show" end end class ProductsController < ActionController::Base def show - render :text => "products#show" + render plain: "products#show" end end @@ -4191,7 +4266,7 @@ end class TestInvalidUrls < ActionDispatch::IntegrationTest class FooController < ActionController::Base def show - render :text => "foo#show" + render plain: "foo#show" end end @@ -4494,7 +4569,7 @@ end class TestDefaultUrlOptions < ActionDispatch::IntegrationTest class PostsController < ActionController::Base def archive - render :text => "posts#archive" + render plain: "posts#archive" end end diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index 22a46b0930..dbb996973d 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -18,11 +18,11 @@ class CacheStoreTest < ActionDispatch::IntegrationTest end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "#{request.session.id}" + render plain: "#{request.session.id}" end def call_reset_session @@ -170,7 +170,7 @@ class CacheStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| @cache = ActiveSupport::Cache::MemoryStore.new middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index e7f4235de8..e432c65c62 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -16,25 +16,25 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def persistent_session_id - render :text => session[:session_id] + render plain: session[:session_id] end def set_session_value session[:foo] = "bar" - render :text => Rack::Utils.escape(Verifier.generate(session.to_hash)) + render plain: Rack::Utils.escape(Verifier.generate(session.to_hash)) end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "id: #{request.session.id}" + render plain: "id: #{request.session.id}" end def get_class_after_reset_session reset_session - render :text => "class: #{session.class}" + render plain: "class: #{session.class}" end def call_session_clear @@ -348,7 +348,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, options - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 9a5d5131c0..3fed9bad4f 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -19,11 +19,11 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "#{request.session.id}" + render plain: "#{request.session.id}" end def call_reset_session @@ -192,7 +192,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}" - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb index d30461a623..59c030176b 100644 --- a/actionpack/test/dispatch/session/test_session_test.rb +++ b/actionpack/test/dispatch/session/test_session_test.rb @@ -40,4 +40,14 @@ class ActionController::TestSessionTest < ActiveSupport::TestCase assert_equal %w(one two), session.keys assert_equal %w(1 2), session.values end + + def test_fetch_returns_default + session = ActionController::TestSession.new(one: '1') + assert_equal('2', session.fetch(:two, '2')) + end + + def test_fetch_returns_block_value + session = ActionController::TestSession.new(one: '1') + assert_equal(2, session.fetch('2') { |key| key.to_i }) + end end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 72eaa916bc..cbb12a2209 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 when "/bad_params" raise ActionDispatch::ParamsParser::ParseError.new("", StandardError.new) when "/method_not_allowed" - raise ActionController::MethodNotAllowed + raise ActionController::MethodNotAllowed, 'PUT' when "/unknown_http_method" raise ActionController::UnknownHttpMethod when "/not_found_original_exception" diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 7ced41bc2e..017e9ba2dd 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -216,4 +216,15 @@ class SSLTest < ActionDispatch::IntegrationTest assert_equal "https://example.co.uk/path?key=value", response.headers['Location'] end + + def test_keeps_original_headers_behavior + headers = Rack::Utils::HeaderHash.new( + "Content-Type" => "text/html", + "Connection" => ["close"] + ) + self.app = ActionDispatch::SSL.new(lambda { |env| [200, headers, ["OK"]] }) + + get "https://example.org/" + assert_equal "close", response.headers["Connection"] + end end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index e729cc44f9..13dec8b618 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -156,7 +156,7 @@ module StaticTests def test_does_not_modify_path_info file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" - env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'} + env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip', "REQUEST_METHOD" => 'POST'} @app.call(env) assert_equal file_name, env['PATH_INFO'] end @@ -263,7 +263,7 @@ class StaticTest < ActiveSupport::TestCase end def test_non_default_static_index - @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60", "other-index") + @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60", index: "other-index") assert_html "/other-index.html", get("/other-index.html") assert_html "/other-index.html", get("/other-index") assert_html "/other-index.html", get("/") diff --git a/actionpack/test/dispatch/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb deleted file mode 100644 index 7278754b49..0000000000 --- a/actionpack/test/dispatch/template_assertions_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'abstract_unit' - -class AssertTemplateController < ActionController::Base - def render_with_partial - render partial: 'test/partial' - end - - def render_with_template - render 'test/hello_world' - end - - def render_with_layout - @variable_for_layout = 'hello' - render 'test/hello_world', layout: "layouts/standard" - end - - def render_with_file - render file: 'README.rdoc' - end - - def render_nothing - head :ok - end -end - -class AssertTemplateControllerTest < ActionDispatch::IntegrationTest - def test_template_reset_between_requests - get '/assert_template/render_with_template' - assert_template 'test/hello_world' - - get '/assert_template/render_nothing' - assert_template nil - end - - def test_partial_reset_between_requests - get '/assert_template/render_with_partial' - assert_template partial: 'test/_partial' - - get '/assert_template/render_nothing' - assert_template partial: nil - end - - def test_layout_reset_between_requests - get '/assert_template/render_with_layout' - assert_template layout: 'layouts/standard' - - get '/assert_template/render_nothing' - assert_template layout: nil - end - - def test_file_reset_between_requests - get '/assert_template/render_with_file' - assert_template file: 'README.rdoc' - - get '/assert_template/render_nothing' - assert_template file: nil - end - - def test_template_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_template' - session.assert_template 'test/hello_world' - - session.get '/assert_template/render_nothing' - session.assert_template nil - end - end - - def test_partial_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_partial' - session.assert_template partial: 'test/_partial' - - session.get '/assert_template/render_nothing' - session.assert_template partial: nil - end - end - - def test_layout_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_layout' - session.assert_template layout: 'layouts/standard' - - session.get '/assert_template/render_nothing' - session.assert_template layout: nil - end - end - - def test_file_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_file' - session.assert_template file: 'README.rdoc' - - session.get '/assert_template/render_nothing' - session.assert_template file: nil - end - end - - def test_assigns_do_not_reset_template_assertion - get '/assert_template/render_with_layout' - assert_equal 'hello', assigns(:variable_for_layout) - assert_template layout: 'layouts/standard' - end - - def test_cookies_do_not_reset_template_assertion - get '/assert_template/render_with_layout' - cookies - assert_template layout: 'layouts/standard' - end -end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index cc35d4594e..ede1cec4e6 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -2,7 +2,7 @@ require 'abstract_unit' class TestRequestTest < ActiveSupport::TestCase test "sane defaults" do - env = ActionDispatch::TestRequest.new.env + env = ActionDispatch::TestRequest.create.env assert_equal "GET", env.delete("REQUEST_METHOD") assert_equal "off", env.delete("HTTPS") @@ -24,12 +24,10 @@ class TestRequestTest < ActiveSupport::TestCase assert_equal true, env.delete("rack.multithread") assert_equal true, env.delete("rack.multiprocess") assert_equal false, env.delete("rack.run_once") - - assert env.empty?, env.inspect end test "cookie jar" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal({}, req.cookies) assert_equal nil, req.env["HTTP_COOKIE"] @@ -57,38 +55,38 @@ class TestRequestTest < ActiveSupport::TestCase test "does not complain when Rails.application is nil" do Rails.stubs(:application).returns(nil) - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal false, req.env.empty? end test "default remote address is 0.0.0.0" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal '0.0.0.0', req.remote_addr end test "allows remote address to be overridden" do - req = ActionDispatch::TestRequest.new('REMOTE_ADDR' => '127.0.0.1') + req = ActionDispatch::TestRequest.create('REMOTE_ADDR' => '127.0.0.1') assert_equal '127.0.0.1', req.remote_addr end test "default host is test.host" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'test.host', req.host end test "allows host to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_HOST' => 'www.example.com') + req = ActionDispatch::TestRequest.create('HTTP_HOST' => 'www.example.com') assert_equal 'www.example.com', req.host end test "default user agent is 'Rails Testing'" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'Rails Testing', req.user_agent end test "allows user agent to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_USER_AGENT' => 'GoogleBot') + req = ActionDispatch::TestRequest.create('HTTP_USER_AGENT' => 'GoogleBot') assert_equal 'GoogleBot', req.user_agent end diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb index dc17668def..a4f9d56a6a 100644 --- a/actionpack/test/dispatch/test_response_test.rb +++ b/actionpack/test/dispatch/test_response_test.rb @@ -11,10 +11,9 @@ class TestResponseTest < ActiveSupport::TestCase end test "helpers" do - assert_response_code_range 200..299, :success? - assert_response_code_range [404], :missing? - assert_response_code_range 300..399, :redirect? - assert_response_code_range 500..599, :error? + assert_response_code_range 200..299, :successful? + assert_response_code_range [404], :not_found? + assert_response_code_range 300..399, :redirection? assert_response_code_range 500..599, :server_error? assert_response_code_range 400..499, :client_error? end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index ce1e1d0a6a..fd4ede4d1b 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -8,7 +8,7 @@ module TestUrlGeneration class ::MyRouteGeneratingController < ActionController::Base include Routes.url_helpers def index - render :text => foo_path + render plain: foo_path end end diff --git a/actionpack/test/fixtures/customers/_commented_customer.html.erb b/actionpack/test/fixtures/customers/_commented_customer.html.erb index d5f6e3b491..8cc9c1ec13 100644 --- a/actionpack/test/fixtures/customers/_commented_customer.html.erb +++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb @@ -1,4 +1,5 @@ <%# I'm a comment %> <% cache customer do %> + <% controller.partial_rendered_times += 1 %> <%= customer.name %>, <%= customer.id %> <% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionpack/test/fixtures/customers/_customer.html.erb index 67e9f6d411..5105090d4b 100644 --- a/actionpack/test/fixtures/customers/_customer.html.erb +++ b/actionpack/test/fixtures/customers/_customer.html.erb @@ -1,3 +1,4 @@ <% cache customer do %> + <% controller.partial_rendered_times += 1 %> <%= customer.name %>, <%= customer.id %> <% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/standard.html.erb b/actionpack/test/fixtures/layouts/standard.html.erb index 5e6c24fe39..48882dca35 100644 --- a/actionpack/test/fixtures/layouts/standard.html.erb +++ b/actionpack/test/fixtures/layouts/standard.html.erb @@ -1 +1 @@ -<html><%= yield %><%= @variable_for_layout %></html>
\ No newline at end of file +<html><%= yield %><%= @variable_for_layout %></html> diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename new file mode 100644 index 0000000000..60738d53b0 --- /dev/null +++ b/actionpack/test/fixtures/multipart/utf8_filename @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%名.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index 6939b426f6..72858f5eda 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -4,6 +4,8 @@ module ActionDispatch module Journey module Path class TestPattern < ActiveSupport::TestCase + SEPARATORS = ["/", ".", "?"].join + x = /.+/ { '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z}, @@ -19,12 +21,12 @@ module ActionDispatch '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))\Z}, }.each do |path, expected| define_method(:"test_to_regexp_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end @@ -43,13 +45,12 @@ module ActionDispatch '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))}, }.each do |path, expected| define_method(:"test_to_non_anchored_regexp_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"], + SEPARATORS, false ) - path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end @@ -67,27 +68,27 @@ module ActionDispatch '/:controller/*foo/bar' => %w{ controller foo }, }.each do |path, expected| define_method(:"test_names_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(expected, path.names) end end def test_to_regexp_with_extended_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => / #ROFL (tender|love #MAO )/x }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/tender') assert_match(path, '/page/love') assert_no_match(path, '/page/loving') @@ -105,23 +106,23 @@ module ActionDispatch end def test_to_regexp_match_non_optional - strexp = Router::Strexp.build( + path = Pattern.build( '/:name', { :name => /\d+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/123') assert_no_match(path, '/') end def test_to_regexp_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => /(tender|love)/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/tender') assert_match(path, '/page/love') assert_no_match(path, '/page/loving') @@ -129,15 +130,13 @@ module ActionDispatch def test_ast_sets_regular_expressions requirements = { :name => /(tender|love)/, :value => /./ } - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/:value', requirements, - ["/", ".", "?"] + SEPARATORS, + true ) - assert_equal requirements, strexp.requirements - - path = Pattern.new strexp nodes = path.ast.grep(Nodes::Symbol) assert_equal 2, nodes.length nodes.each do |node| @@ -146,24 +145,24 @@ module ActionDispatch end def test_match_data_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => /(tender|love)/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp match = path.match '/page/tender' assert_equal 'tender', match[1] assert_equal 2, match.length end def test_match_data_with_multi_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/:id', { :name => /t(((ender|love)))()/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp match = path.match '/page/tender/10' assert_equal 'tender', match[1] assert_equal '10', match[2] @@ -173,30 +172,29 @@ module ActionDispatch def test_star_with_custom_re z = /\d+/ - strexp = Router::Strexp.build( + path = Pattern.build( '/page/*foo', { :foo => z }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp) end def test_insensitive_regexp_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/aaron', { :name => /(tender|love)/i }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/TENDER/aaron') assert_match(path, '/page/loVE/aaron') assert_no_match(path, '/page/loVE/AAron') end def test_to_regexp_with_strexp - strexp = Router::Strexp.build('/:controller', { }, ["/", ".", "?"]) - path = Pattern.new strexp + path = Pattern.build('/:controller', { }, SEPARATORS, true) x = %r{\A/([^/.?]+)\Z} assert_equal(x.source, path.source) diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index 0980df06e8..22c3b8113d 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -7,7 +7,7 @@ module ActionDispatch app = Object.new path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} - route = Route.new("name", app, path, {}, defaults) + route = Route.build("name", app, path, {}, [], defaults) assert_equal app, route.app assert_equal path, route.path @@ -18,7 +18,7 @@ module ActionDispatch app = Object.new path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} - route = Route.new("name", app, path, {}, defaults) + route = Route.build("name", app, path, {}, [], defaults) route.ast.grep(Nodes::Terminal).each do |node| assert_equal route, node.memo @@ -26,30 +26,29 @@ module ActionDispatch end def test_path_requirements_override_defaults - strexp = Router::Strexp.build(':name', { name: /love/ }, ['/']) - path = Path::Pattern.new strexp + path = Path::Pattern.build(':name', { name: /love/ }, '/', true) defaults = { name: 'tender' } - route = Route.new('name', nil, path, nil, defaults) + route = Route.build('name', nil, path, {}, [], defaults) assert_equal(/love/, route.requirements[:name]) end def test_ip_address path = Path::Pattern.from_string '/messages/:id(.:format)' - route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, + route = Route.build("name", nil, path, {:ip => '192.168.1.1'}, [], { :controller => 'foo', :action => 'bar' }) assert_equal '192.168.1.1', route.ip end def test_default_ip path = Path::Pattern.from_string '/messages/:id(.:format)' - route = Route.new("name", nil, path, {}, + route = Route.build("name", nil, path, {}, [], { :controller => 'foo', :action => 'bar' }) assert_equal(//, route.ip) end def test_format_with_star path = Path::Pattern.from_string '/:controller/*extra' - route = Route.new("name", nil, path, {}, + route = Route.build("name", nil, path, {}, [], { :controller => 'foo', :action => 'bar' }) assert_equal '/foo/himom', route.format({ :controller => 'foo', @@ -59,7 +58,7 @@ module ActionDispatch def test_connects_all_match path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' - route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' }) + route = Route.build("name", nil, path, {:action => 'bar'}, [], { :controller => 'foo' }) assert_equal '/foo/bar/10', route.format({ :controller => 'foo', @@ -70,34 +69,34 @@ module ActionDispatch def test_extras_are_not_included_if_optional path = Path::Pattern.from_string '/page/:id(/:action)' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/page/10', route.format({ :id => 10 }) end def test_extras_are_not_included_if_optional_with_parameter path = Path::Pattern.from_string '(/sections/:section)/pages/:id' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10}) end def test_extras_are_not_included_if_optional_parameter_is_nil path = Path::Pattern.from_string '(/sections/:section)/pages/:id' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10, :section => nil}) end def test_score - constraints = {:required_defaults => [:controller, :action]} + constraints = {} defaults = {:controller=>"pages", :action=>"show"} path = Path::Pattern.from_string "/page/:id(/:action)(.:format)" - specific = Route.new "name", nil, path, constraints, defaults + specific = Route.build "name", nil, path, constraints, [:controller, :action], defaults path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)" - generic = Route.new "name", nil, path, constraints + generic = Route.build "name", nil, path, constraints, [], {} knowledge = {:id=>20, :controller=>"pages", :action=>"show"} diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index a134e343cc..d512dae4e7 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -4,138 +4,43 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase - attr_reader :routes + attr_reader :routes, :mapper def setup @app = Routing::RouteSet::Dispatcher.new({}) - @routes = Routes.new - @router = Router.new(@routes) - @formatter = Formatter.new(@routes) - end - - class FakeRequestFeeler < Struct.new(:env, :called) - def new env - self.env = env - self - end - - def hello - self.called = true - 'world' - end - - def path_info; env['PATH_INFO']; end - def request_method; env['REQUEST_METHOD']; end - def ip; env['REMOTE_ADDR']; end + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @formatter = @route_set.formatter + @mapper = ActionDispatch::Routing::Mapper.new @route_set end def test_dashes - router = Router.new(routes) - - exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {:id => nil}, {} + mapper.get '/foo-bar-baz', to: 'foo#bar' env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false - router.recognize(env) do |r, params| + @router.recognize(env) do |r, params| called = true end assert called end def test_unicode - router = Router.new(routes) + mapper.get '/ほげ', to: 'foo#bar' #match the escaped version of /ほげ - exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false - router.recognize(env) do |r, params| + @router.recognize(env) do |r, params| called = true end assert called end - def test_request_class_and_requirements_success - klass = FakeRequestFeeler.new nil - router = Router.new(routes) - - requirements = { :hello => /world/ } - - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, requirements, {:id => nil}, {} - - env = rails_env({'PATH_INFO' => '/foo/10'}, klass) - router.recognize(env) do |r, params| - assert_equal({:id => '10'}, params) - end - - assert klass.called, 'hello should have been called' - assert_equal env.env, klass.env - end - - def test_request_class_and_requirements_fail - klass = FakeRequestFeeler.new nil - router = Router.new(routes) - - requirements = { :hello => /mom/ } - - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - - router.routes.add_route nil, path, requirements, {:id => nil}, {} - - env = rails_env({'PATH_INFO' => '/foo/10'}, klass) - router.recognize(env) do |r, params| - flunk 'route should not be found' - end - - assert klass.called, 'hello should have been called' - assert_equal env.env, klass.env - end - - class CustomPathRequest < ActionDispatch::Request - def path_info - env['custom.path_info'] - end - - def path_info=(x) - env['custom.path_info'] = x - end - end - - def test_request_class_overrides_path_info - router = Router.new(routes) - - exp = Router::Strexp.build '/bar', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {}, {} - - env = rails_env({'PATH_INFO' => '/foo', - 'custom.path_info' => '/bar'}, CustomPathRequest) - - recognized = false - router.recognize(env) do |r, params| - recognized = true - end - - assert recognized, "route should have been recognized" - end - def test_regexp_first_precedence - add_routes @router, [ - Router::Strexp.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), - Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?']) - ] + mapper.get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar" + mapper.get "/whois/:id(.:format)", to: "foo#baz" env = rails_env 'PATH_INFO' => '/whois/example.com' @@ -147,25 +52,21 @@ module ActionDispatch r = list.first - assert_equal '/whois/:domain', r.path.spec.to_s + assert_equal '/whois/:domain(.:format)', r.path.spec.to_s end def test_required_parts_verified_are_anchored - add_routes @router, [ - Router::Strexp.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) - ] + mapper.get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar" assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(nil, { :id => '10' }, { }) + @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) end end def test_required_parts_are_verified_when_building - add_routes @router, [ - Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) - ] + mapper.get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar" - path, _ = @formatter.generate(nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path assert_raises(ActionController::UrlGenerationError) do @@ -174,25 +75,22 @@ module ActionDispatch end def test_only_required_parts_are_verified - add_routes @router, [ - Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) - ] + mapper.get "/foo(/:id)", :id => /\d/, :to => "foo#bar" - path, _ = @formatter.generate(nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path - path, _ = @formatter.generate(nil, { }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { }) assert_equal '/foo', path - path, _ = @formatter.generate(nil, { :id => 'aa' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => 'aa' }, { }) assert_equal '/foo/aa', path end def test_knows_what_parts_are_missing_from_named_route route_name = "gorby_thunderhorse" - pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) - path = Path::Pattern.new pattern - @router.routes.add_route nil, path, {}, {}, route_name + mapper = ActionDispatch::Routing::Mapper.new @route_set + mapper.get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar" error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate(route_name, { }, { }) @@ -212,7 +110,7 @@ module ActionDispatch end def test_X_Cascade - add_routes @router, [ "/messages(.:format)" ] + mapper.get "/messages(.:format)", to: "foo#bar" resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) assert_equal ['Not Found'], resp.last assert_equal 'pass', resp[1]['X-Cascade'] @@ -233,24 +131,21 @@ module ActionDispatch end def test_defaults_merge_correctly - path = Path::Pattern.from_string '/foo(/:id)' - @router.routes.add_route nil, path, {}, {:id => nil}, {} + mapper.get '/foo(/:id)', to: "foo#bar", id: nil env = rails_env 'PATH_INFO' => '/foo/10' @router.recognize(env) do |r, params| - assert_equal({:id => '10'}, params) + assert_equal({:id => '10', :controller => "foo", :action => "bar"}, params) end env = rails_env 'PATH_INFO' => '/foo' @router.recognize(env) do |r, params| - assert_equal({:id => nil}, params) + assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params) end end def test_recognize_with_unbound_regexp - add_routes @router, [ - Router::Strexp.build("/foo", { }, ['/', '.', '?'], false) - ] + mapper.get "/foo", anchor: false, to: "foo#bar" env = rails_env 'PATH_INFO' => '/foo/bar' @@ -261,9 +156,7 @@ module ActionDispatch end def test_bound_regexp_keeps_path_info - add_routes @router, [ - Router::Strexp.build("/foo", { }, ['/', '.', '?'], true) - ] + mapper.get "/foo", to: "foo#bar" env = rails_env 'PATH_INFO' => '/foo' @@ -276,12 +169,14 @@ module ActionDispatch end def test_path_not_found - add_routes @router, [ + [ "/messages(.:format)", "/messages/new(.:format)", "/messages/:id/edit(.:format)", "/messages/:id(.:format)" - ] + ].each do |path| + mapper.get path, to: "foo#bar" + end env = rails_env 'PATH_INFO' => '/messages/unknown/path' yielded = false @@ -292,32 +187,29 @@ module ActionDispatch end def test_required_part_in_recall - add_routes @router, [ "/messages/:a/:b" ] + mapper.get "/messages/:a/:b", to: "foo#bar" - path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall - add_routes @router, [ "/*path" ] + mapper.get "/*path", to: "foo#bar" - path, _ = @formatter.generate(nil, { }, { :path => 'b' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :path => 'b' }) assert_equal "/b", path end def test_recall_should_be_used_when_scoring - add_routes @router, [ - "/messages/:action(/:id(.:format))", - "/messages/:id(.:format)" - ] + mapper.get "/messages/:action(/:id(.:format))", to: 'foo#bar' + mapper.get "/messages/:id(.:format)", to: 'bar#baz' - path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end def test_nil_path_parts_are_ignored - path = Path::Pattern.from_string "/:controller(/:action(.:format))" - @router.routes.add_route @app, path, {}, {}, {} + mapper.get "/:controller(/:action(.:format))", to: "tasks#lol" params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } @@ -329,18 +221,14 @@ module ActionDispatch def test_generate_slash params = [ [:controller, "tasks"], [:action, "show"] ] - str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true) - path = Path::Pattern.new str - - @router.routes.add_route @app, path, {}, {}, {} + mapper.get "/", Hash[params] path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end def test_generate_calls_param_proc - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action)', to: "foo#bar" parameterized = [] params = [ [:controller, "tasks"], @@ -356,8 +244,7 @@ module ActionDispatch end def test_generate_id - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action)', to: 'foo#bar' path, params = @formatter.generate( nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) @@ -366,8 +253,7 @@ module ActionDispatch end def test_generate_escapes - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "tasks", @@ -377,8 +263,7 @@ module ActionDispatch end def test_generate_escapes_with_namespaced_controller - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate( nil, { :controller => "admin/tasks", @@ -388,8 +273,7 @@ module ActionDispatch end def test_generate_extra_params - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action)', to: "foo#bar" path, params = @formatter.generate( nil, { :id => 1, @@ -402,8 +286,7 @@ module ActionDispatch end def test_generate_missing_keys_no_matches_different_format_keys - path = Path::Pattern.from_string '/:controller/:action/:name' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller/:action/:name', to: "foo#bar" primarty_parameters = { :id => 1, :controller => "tasks", @@ -429,8 +312,7 @@ module ActionDispatch end def test_generate_uses_recall_if_needed - path = Path::Pattern.from_string '/:controller(/:action(/:id))' - @router.routes.add_route @app, path, {}, {}, {} + mapper.get '/:controller(/:action(/:id))', to: "foo#bar" path, params = @formatter.generate( nil, @@ -441,8 +323,7 @@ module ActionDispatch end def test_generate_with_name - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, "tasks" + mapper.get '/:controller(/:action)', to: 'foo#bar', as: 'tasks' path, params = @formatter.generate( "tasks", @@ -458,16 +339,15 @@ module ActionDispatch '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" }, }.each do |request_path, expected| define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do - path = Path::Pattern.from_string "/:controller(/:action(/:id))" - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + mapper.get "/:controller(/:action(/:id))", to: 'foo#bar' + route = @routes.first env = rails_env 'PATH_INFO' => request_path called = false @router.recognize(env) do |r, params| assert_equal route, r - assert_equal(expected, params) + assert_equal({ :action => "bar" }.merge(expected), params) called = true end @@ -480,16 +360,15 @@ module ActionDispatch :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }] }.each do |name, (request_path, expected)| define_method("test_recognize_#{name}") do - path = Path::Pattern.from_string '/:segment/*splat' - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + mapper.get '/:segment/*splat', to: 'foo#bar' env = rails_env 'PATH_INFO' => request_path called = false + route = @routes.first @router.recognize(env) do |r, params| assert_equal route, r - assert_equal(expected, params) + assert_equal(expected.merge(:controller=>"foo", :action=>"bar"), params) called = true end @@ -498,14 +377,8 @@ module ActionDispatch end def test_namespaced_controller - strexp = Router::Strexp.build( - "/:controller(/:action(/:id))", - { :controller => /.+?/ }, - ["/", ".", "?"] - ) - path = Path::Pattern.new strexp - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + mapper.get "/:controller(/:action(/:id))", { :controller => /.+?/ } + route = @routes.first env = rails_env 'PATH_INFO' => '/admin/users/show/10' called = false @@ -524,9 +397,8 @@ module ActionDispatch end def test_recognize_literal - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) + mapper.get "/books(/:action(.:format))", controller: "books" + route = @routes.first env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } @@ -541,10 +413,7 @@ module ActionDispatch end def test_recognize_head_route - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { request_method: 'HEAD' } - @router.routes.add_route(app, path, conditions, {}) + mapper.match "/books(/:action(.:format))", via: 'head', to: 'foo#bar' env = rails_env( 'PATH_INFO' => '/books/list.rss', @@ -560,12 +429,7 @@ module ActionDispatch end def test_recognize_head_request_as_get_route - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { - :request_method => 'GET' - } - @router.routes.add_route(app, path, conditions, {}) + mapper.get "/books(/:action(.:format))", to: 'foo#bar' env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "HEAD" @@ -578,11 +442,8 @@ module ActionDispatch assert called end - def test_recognize_cares_about_verbs - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { request_method: 'GET' } - @router.routes.add_route(app, path, conditions, {}) + def test_recognize_cares_about_get_verbs + mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :get env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "POST" @@ -593,31 +454,58 @@ module ActionDispatch end assert_not called + end - conditions = conditions.dup - conditions[:request_method] = 'POST' + def test_recognize_cares_about_post_verbs + mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :post - post = @router.routes.add_route(app, path, conditions, {}) + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => "POST" called = false @router.recognize(env) do |r, params| - assert_equal post, r called = true end assert called end + def test_multi_verb_recognition + mapper.match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get] + + %w( POST GET ).each do |verb| + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => verb + + called = false + @router.recognize(env) do |r, params| + called = true + end + + assert called + end + + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => 'PUT' + + called = false + @router.recognize(env) do |r, params| + called = true + end + + assert_not called + end + private - def add_routes router, paths + def add_routes router, paths, anchor = true paths.each do |path| if String === path path = Path::Pattern.from_string path else - path = Path::Pattern.new path + path end - router.routes.add_route @app, path, {}, {}, {} + add_route @app, path, {}, [], {}, {} end end diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb index 2a1c1c337e..f8293dfc5f 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -3,18 +3,19 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRoutes < ActiveSupport::TestCase - setup do - @routes = Routes.new + attr_reader :routes, :mapper + + def setup + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @mapper = ActionDispatch::Routing::Mapper.new @route_set + super end def test_clear - routes = Routes.new - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - requirements = { :hello => /world/ } - - routes.add_route nil, path, requirements, {:id => nil}, {} - assert_not routes.empty? + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' + assert_not_predicate routes, :empty? assert_equal 1, routes.length routes.clear @@ -23,52 +24,36 @@ module ActionDispatch end def test_ast - routes = Routes.new - path = Path::Pattern.from_string '/hello' - - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' ast = routes.ast - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby' assert_not_equal ast, routes.ast end def test_simulator_changes - routes = Routes.new - path = Path::Pattern.from_string '/hello' - - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' sim = routes.simulator - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby' assert_not_equal sim, routes.simulator end def test_partition_route - path = Path::Pattern.from_string '/hello' + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' - anchored_route = @routes.add_route nil, path, {}, {}, {} - assert_equal [anchored_route], @routes.anchored_routes - assert_equal [], @routes.custom_routes + assert_equal 1, @routes.anchored_routes.length + assert_predicate @routes.custom_routes, :empty? - strexp = Router::Strexp.build( - "/hello/:who", { who: /\d/ }, ['/', '.', '?'] - ) - path = Path::Pattern.new strexp + mapper.get "/hello/:who", to: "foo#bar", as: 'bar', who: /\d/ - custom_route = @routes.add_route nil, path, {}, {}, {} - assert_equal [custom_route], @routes.custom_routes - assert_equal [anchored_route], @routes.anchored_routes + assert_equal 1, @routes.custom_routes.length + assert_equal 1, @routes.anchored_routes.length end def test_first_name_wins - routes = Routes.new - - one = Path::Pattern.from_string '/hello' - two = Path::Pattern.from_string '/aaron' - - routes.add_route nil, one, {}, {}, 'aaron' - routes.add_route nil, two, {}, {}, 'aaron' - - assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s + mapper.get "/hello", to: "foo#bar", as: 'aaron' + assert_raise(ArgumentError) do + mapper.get "/aaron", to: "foo#bar", as: 'aaron' + end end end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 201e118971..90c9c2171c 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,58 @@ +* Make `disable_with` the default behavior for submit tags. Disables the + button on submit to prevent double submits. + + *Justin Schiff* + +* Add a break_sequence option to word_wrap so you can specify a custom break. + + * Mauricio Gomez * + +* Add wildcard matching to explicit dependencies. + + Turns: + + ```erb + <% # Template Dependency: recordings/threads/events/subscribers_changed %> + <% # Template Dependency: recordings/threads/events/completed %> + <% # Template Dependency: recordings/threads/events/uncompleted %> + ``` + + Into: + + ```erb + <% # Template Dependency: recordings/threads/events/* %> + ``` + + *Kasper Timm Hansen* + +* Allow defining explicit collection caching using a `# Template Collection: ...` + directive inside templates. + + *Dov Murik* + +* Asset helpers raise `ArgumentError` when `nil` is passed as a source. + + *Anton Kolomiychuk* + +* Always attach the template digest to the cache key for collection caching + even when `virtual_path` is not available from the view context. + Which could happen if the rendering was done directly in the controller + and not in a template. + + Fixes #20535 + + *Roque Pinel* + +* Improve detection of partial templates eligible for collection caching, + now allowing multi-line comments at the beginning of the template file. + + *Dov Murik* + +* Raise an ArgumentError when a false value for `include_blank` is passed to a + required select field (to comply with the HTML5 spec). + + *Grey Baker* + * Do not put partial name to `local_assigns` when rendering without an object or a collection. diff --git a/actionview/bin/test b/actionview/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionview/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 43124bb904..72ca6f7ec6 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -161,6 +161,10 @@ module ActionView #:nodoc: cattr_accessor :raise_on_missing_translations @@raise_on_missing_translations = false + # Specify whether submit_tag should automatically disable on click + cattr_accessor :automatically_disable_submit_tag + @@automatically_disable_submit_tag = true + class_attribute :_routes class_attribute :logger diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 7a7e116dbb..6e8c7f8203 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -1,16 +1,18 @@ require 'thread_safe' +require 'action_view/path_set' module ActionView class DependencyTracker # :nodoc: @trackers = ThreadSafe::Cache.new - def self.find_dependencies(name, template) + def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] + return [] unless tracker.present? - if tracker.present? - tracker.call(name, template) + if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths? + tracker.call(name, template, view_paths) else - [] + tracker.call(name, template) end end @@ -82,12 +84,16 @@ module ActionView (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm - def self.call(name, template) - new(name, template).dependencies + def self.supports_view_paths? # :nodoc: + true + end + + def self.call(name, template, view_paths = nil) + new(name, template, view_paths).dependencies end - def initialize(name, template) - @name, @template = name, template + def initialize(name, template, view_paths = nil) + @name, @template, @view_paths = name, template, view_paths end def dependencies @@ -142,8 +148,22 @@ module ActionView end end + def resolve_directories(wildcard_dependencies) + return [] unless @view_paths + + wildcard_dependencies.each_with_object([]) do |query, templates| + @view_paths.find_all_with_query(query).each do |template| + templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}" + end + end + end + def explicit_dependencies - source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + + wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' } + + (explicits + resolve_directories(wildcards)).uniq end end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1f103786cb..9a8a4feb2e 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -8,11 +8,18 @@ module ActionView @@cache = ThreadSafe::Cache.new @@digest_monitor = Monitor.new + class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: + def call(env) + ActionView::Digestor.cache.clear + app.call(env) + end + end + class << self # Supported options: # # * <tt>name</tt> - Template name - # * <tt>finder</tt> - An instance of ActionView::LookupContext + # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial def digest(options) @@ -41,10 +48,7 @@ module ActionView Digestor end - digest = klass.new(options).digest - # Store the actual digest if config.cache_template_loading is true - @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? - digest + @@cache[cache_key] = stored_digest = klass.new(options).digest ensure # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest @@ -68,9 +72,10 @@ module ActionView end def dependencies - DependencyTracker.find_dependencies(name, template) + DependencyTracker.find_dependencies(name, template, finder.view_paths) rescue ActionView::MissingTemplate - [] # File doesn't exist, so no dependencies + logger.try :error, " '#{name}' file doesn't exist, so no dependencies" + [] end def nested_dependencies diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index e32f8e219e..e506c782d6 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -60,7 +60,7 @@ module ActionView tag_options = { "src" => path_to_javascript(source, path_options) }.merge!(options) - content_tag(:script, "", tag_options) + content_tag("script".freeze, "", tag_options) }.join("\n").html_safe end @@ -237,7 +237,7 @@ module ActionView # image_alt('underscored_file_name.png') # # => Underscored file name def image_alt(src) - File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize + File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize end # Returns an HTML video tag for the +sources+. If +sources+ is a string, diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index ef4a6c98c0..717b326740 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -31,26 +31,33 @@ module ActionView # stylesheet_link_tag("application") # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # - # Browsers typically open at most two simultaneous connections to a single - # host, which means your assets often have to wait for other assets to finish - # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the - # +asset_host+. For example, "assets%d.example.com". If that wildcard is - # present Rails distributes asset requests among the corresponding four hosts - # "assets0.example.com", ..., "assets3.example.com". With this trick browsers - # will open eight simultaneous connections rather than two. + # Browsers open a limited number of simultaneous connections to a single + # host. The exact number varies by browser and version. This limit may cause + # some asset downloads to wait for previous assets to finish before they can + # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to + # distribute the requests over four hosts. For example, + # <tt>assets%d.example.com<tt> will spread the asset requests over + # "assets0.example.com", ..., "assets3.example.com". # # image_tag("rails.png") # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # - # To do this, you can either setup four actual hosts, or you can use wildcard - # DNS to CNAME the wildcard to a single asset host. You can read more about - # setting up your DNS CNAME records from your ISP. + # This may improve the asset loading performance of your application. + # It is also possible the combination of additional connection overhead + # (DNS, SSL) and the overall browser connection limits may result in this + # solution being slower. You should be sure to measure your actual + # performance across targeted browsers both before and after this change. + # + # To implement the corresponding hosts you can either setup four actual + # hosts or use wildcard DNS to CNAME the wildcard to a single asset host. + # You can read more about setting up your DNS CNAME records from your ISP. # # Note: This is purely a browser performance optimization and is not meant # for server load balancing. See http://www.die.net/musings/page_load_time/ - # for background. + # for background and http://www.browserscope.org/?category=network for + # connection limit data. # # Alternatively, you can exert more control over the asset host by setting # +asset_host+ to a proc like this: @@ -121,11 +128,13 @@ module ActionView # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) + raise ArgumentError, "nil is not a valid asset source" if source.nil? + source = source.to_s return "" unless source.present? return source if source =~ URI_REGEXP - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze) if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 251764a8de..636d7d4cc3 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -39,7 +39,7 @@ module ActionView # This will include both records as part of the cache key and updating either of them will # expire the cache. # - # ==== Template digest + # ==== \Template digest # # The template digest that's added to the cache key is computed by taking an md5 of the # contents of the entire template file. This ensures that your caches will automatically @@ -75,7 +75,7 @@ module ActionView # render(topics) => render("topics/topic") # render(message.topics) => render("topics/topic") # - # It's not possible to derive all render calls like that, though. + # It's not possible to derive all render calls like that, though. # Here are a few examples of things that can't be derived: # # render group_of_attachments @@ -98,14 +98,26 @@ module ActionView # <%# Template Dependency: todolists/todolist %> # <%= render_sortable_todolists @project.todolists %> # - # The pattern used to match these is /# Template Dependency: ([^ ]+)/, + # In some cases, like a single table inheritance setup, you might have + # a bunch of explicit dependencies. Instead of writing every template out, + # you can use a wildcard to match any template in a directory: + # + # <%# Template Dependency: events/* %> + # <%= render_categorizable_events @person.events %> + # + # This marks every template in the directory as a dependency. To find those + # templates, the wildcard path must be absolutely defined from app/views or paths + # otherwise added with +prepend_view_path+ or +append_view_path+. + # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc. + # + # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>, # so it's important that you type it out just so. # You can only declare one template dependency per line. # # === External dependencies # - # If you use a helper method, for example, inside a cached block and - # you then update that helper, you'll have to bump the cache as well. + # If you use a helper method, for example, inside a cached block and + # you then update that helper, you'll have to bump the cache as well. # It doesn't really matter how you do it, but the md5 of the template file # must change. One recommendation is to simply be explicit in a comment, like: # @@ -130,14 +142,29 @@ module ActionView # The collection can then automatically use any cached renders for that # template by reading them at once instead of one by one. # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for - # more information on what cache calls make a template eligible for this + # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for + # more information on what cache calls make a template eligible for this # collection caching. # # The automatic cache multi read can be turned off like so: # # <%= render @notifications, cache: false %> - def cache(name = {}, options = nil, &block) + # + # === Explicit Collection Caching + # + # If the partial template doesn't start with a clean cache call as + # mentioned above, you can still benefit from collection caching by + # adding a special comment format anywhere in the template, like: + # + # <%# Template Collection: notification %> + # <% my_helper_that_calls_cache(some_arg, notification) do %> + # <%= notification.name %> + # <% end %> + # + # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, + # so it's important that you type it out just so. + # You can only declare one collection in a partial template file. + def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) else @@ -153,7 +180,7 @@ module ActionView # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> - def cache_if(condition, name = {}, options = nil, &block) + def cache_if(condition, name = {}, options = {}, &block) if condition cache(name, options, &block) else @@ -169,22 +196,23 @@ module ActionView # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> - def cache_unless(condition, name = {}, options = nil, &block) + def cache_unless(condition, name = {}, options = {}, &block) cache_if !condition, name, options, &block end # This helper returns the name of a cache key for a given fragment cache - # call. By supplying skip_digest: true to cache, the digestion of cache + # call. By supplying +skip_digest:+ true to cache, the digestion of cache # fragments can be manually bypassed. This is useful when cache fragments # cannot be manually expired unless you know the exact key which is the # case when using memcached. - def cache_fragment_name(name = {}, options = nil) - skip_digest = options && options[:skip_digest] - + # + # The digest will be generated using +virtual_path:+ if it is provided. + # + def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil) if skip_digest name else - fragment_name_with_digest(name) + fragment_name_with_digest(name, virtual_path) end end @@ -198,10 +226,11 @@ module ActionView private - def fragment_name_with_digest(name) #:nodoc: - if @virtual_path + def fragment_name_with_digest(name, virtual_path) #:nodoc: + virtual_path ||= @virtual_path + if virtual_path names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name) - digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies + digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies [ *names, digest ] else diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index a67ba580f1..93c7cba395 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -115,7 +115,7 @@ module ActionView # <li><%= link_to 'Home', action: 'index' %></li> # <% end %> # - # And in other place: + # And in another place: # # <% content_for :navigation do %> # <li><%= link_to 'Login', action: 'login' %></li> diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 46ce7cf0be..312e41ee48 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -68,6 +68,27 @@ module ActionView # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years # distance_of_time_in_words(Time.now, Time.now) # => less than a minute + # + # With the <tt>scope</tt> option, you can define a custom scope for Rails + # to look up the translation. + # + # For example you can define the following in your locale (e.g. en.yml). + # + # datetime: + # distance_in_words: + # short: + # about_x_hours: + # one: 'an hour' + # other: '%{count} hours' + # + # See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml + # for more examples. + # + # Which will then result in the following: + # + # from_time = Time.now + # distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour" + # distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours" def distance_of_time_in_words(from_time, to_time = 0, options = {}) options = { scope: :'datetime.distance_in_words' @@ -207,6 +228,7 @@ module ActionView # or the given prompt string. # * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option # automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags. + # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -464,7 +486,7 @@ module ActionView # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'second' by default. # - # my_time = Time.now + 16.minutes + # my_time = Time.now + 16.seconds # # # Generates a select field for seconds that defaults to the seconds for the time in my_time. # select_second(my_time) @@ -488,7 +510,7 @@ module ActionView # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # - # my_time = Time.now + 6.hours + # my_time = Time.now + 10.minutes # # # Generates a select field for minutes that defaults to the minutes for the time in my_time. # select_minute(my_time) @@ -660,7 +682,7 @@ module ActionView content = args.first || I18n.l(date_or_time, :format => format) datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 - content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block) + content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block) end end @@ -788,7 +810,7 @@ module ActionView 1.upto(12) do |month_number| options = { :value => month_number } options[:selected] = "selected" if month == month_number - month_options << content_tag(:option, month_name(month_number), options) + "\n" + month_options << content_tag("option".freeze, month_name(month_number), options) + "\n" end build_select(:month, month_options.join) end @@ -950,7 +972,7 @@ module ActionView tag_options[:selected] = "selected" if selected == i text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value text = options[:ampm] ? AMPM_TRANSLATION[i] : text - select_options << content_tag(:option, text, tag_options) + select_options << content_tag("option".freeze, text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -970,11 +992,11 @@ module ActionView select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes] select_html = "\n" - select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html - (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe + (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe end # Builds a prompt option tag with supplied options or from default options. @@ -991,7 +1013,7 @@ module ActionView I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale]) end - prompt ? content_tag(:option, prompt, :value => '') : '' + prompt ? content_tag("option".freeze, prompt, :value => '') : '' end # Builds hidden input tag for date part and value. diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index d3deee0df3..728e633bbf 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -35,8 +35,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value="">None</option> # <option value="1">David</option> - # <option value="2" selected="selected">Sam</option> - # <option value="3">Tobias</option> + # <option value="2" selected="selected">Eileen</option> + # <option value="3">Rafael</option> # </select> # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. @@ -48,8 +48,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value="">Select Person</option> # <option value="1">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> + # <option value="2">Eileen</option> + # <option value="3">Rafael</option> # </select> # # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this @@ -112,8 +112,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value=""></option> # <option value="1" selected="selected">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> + # <option value="2">Eileen</option> + # <option value="3">Rafael</option> # </select> # # assuming the associated person has ID 1. @@ -456,7 +456,7 @@ module ActionView option_tags = options_from_collection_for_select( group.send(group_method), option_key_method, option_value_method, selected_key) - content_tag(:optgroup, option_tags, label: group.send(group_label_method)) + content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method)) end.join.html_safe end @@ -528,7 +528,7 @@ module ActionView body = "".html_safe if prompt - body.safe_concat content_tag(:option, prompt_text(prompt), value: "") + body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "") end grouped_options.each do |container| @@ -541,7 +541,7 @@ module ActionView end html_attributes = { label: label }.merge!(html_attributes) - body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes) + body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes) end body @@ -577,7 +577,7 @@ module ActionView end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) - zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true) + zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -707,6 +707,27 @@ module ActionView # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| # b.label(:"data-value" => b.value) { b.check_box + b.text } # end + # + # ==== Gotcha + # + # When no selection is made for a collection of checkboxes most + # web browsers will not send any value. + # + # For example, if we have a +User+ model with +category_ids+ field and we + # have the following code in our update action: + # + # @user.update(params[:user]) + # + # If no +category_ids+ are selected then we can safely assume this field + # will not be updated. + # + # This is possible thanks to a hidden field generated by the helper method + # for every collection of checkboxes. + # This hidden field is given the same field name as the checkboxes with a + # blank value. + # + # In the rare case you don't want this hidden field, you can pass the + # <tt>include_hidden: false</tt> option to the helper method. def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 1f76f40138..af684e05c6 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -140,15 +140,15 @@ module ActionView end if include_blank - option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags) + option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags) end end if prompt = options.delete(:prompt) - option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags) + option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags) end - content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) + content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) end # Creates a standard text field; use these text fields to input smaller chunks of text like a username @@ -414,42 +414,57 @@ module ActionView # the form 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. + # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag + # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute. # # ==== Examples # submit_tag - # # => <input name="commit" type="submit" value="Save changes" /> + # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" /> # # submit_tag "Edit this article" - # # => <input name="commit" type="submit" value="Edit this article" /> + # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" /> # # submit_tag "Save edits", disabled: true - # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" /> + # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" /> # - # submit_tag "Complete sale", data: { disable_with: "Please wait..." } - # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" /> + # submit_tag "Complete sale", data: { disable_with: "Submitting..." } + # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" /> # # submit_tag nil, class: "form_submit" # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", class: "edit_button" - # # => <input class="edit_button" name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" /> # # submit_tag "Save", data: { confirm: "Are you sure?" } - # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> + # # => <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 + 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.clone + tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) + else + tag_options.delete("data-disable-with") + tag_options["data"].delete(:disable_with) if tag_options["data"] + end + end - tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(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 # 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 allows richer labels such as images and emphasis, - # so this helper will also accept a block. + # the button tag does allow for richer labels such as images and emphasis, + # so this helper will also accept a block. By default, it will create + # a button tag with type `submit`, if type is not given. # # ==== Options # * <tt>:data</tt> - This option can be used to add custom data attributes. @@ -472,6 +487,15 @@ module ActionView # button_tag # # => <button name="button" type="submit">Button</button> # + # button_tag 'Reset', type: 'reset' + # # => <button name="button" type="reset">Reset</button> + # + # button_tag 'Button', type: 'button' + # # => <button name="button" type="button">Button</button> + # + # button_tag 'Reset', type: 'reset', disabled: true + # # => <button name="button" type="reset" disabled="disabled">Reset</button> + # # button_tag(type: 'button') do # content_tag(:strong, 'Ask me!') # end @@ -479,6 +503,9 @@ module ActionView # # <strong>Ask me!</strong> # # </button> # + # button_tag "Save", data: { confirm: "Are you sure?" } + # # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button> + # # button_tag "Checkout", data: { disable_with: "Please wait..." } # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> # @@ -555,7 +582,7 @@ module ActionView # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, options = nil, &block) output = tag(:fieldset, options, true) - output.safe_concat(content_tag(:legend, legend)) unless legend.blank? + output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank? output.concat(capture(&block)) if block_given? output.safe_concat("</fieldset>") end diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index e237a32cb7..ed7e882c94 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -47,8 +47,8 @@ module ActionView # tag. # # javascript_tag "alert('All is good')", defer: 'defer' - # - # Returns: + # + # Returns: # <script defer="defer"> # //<![CDATA[ # alert('All is good') @@ -70,7 +70,7 @@ module ActionView content_or_options_with_block end - content_tag(:script, javascript_cdata_section(content), html_options) + content_tag("script".freeze, javascript_cdata_section(content), html_options) end def javascript_cdata_section(content) #:nodoc: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index ca8d30e4ef..13effa592d 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -279,7 +279,7 @@ module ActionView # See <tt>number_to_human_size</tt> if you want to print a file # size. # - # You can also define you own unit-quantifier names if you want + # You can also define your own unit-quantifier names if you want # to use other decimal units (eg.: 1500 becomes "1.5 # kilometers", 0.150 becomes "150 milliliters", etc). You may # define a wide range of unit quantifiers, even fractional ones diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 827932d8e2..c98f2d74a8 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -18,7 +18,7 @@ module ActionView # performs HTML escape on the string first. Setting the content type as # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt> # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index a2e9f37453..191a881de0 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -120,7 +120,7 @@ module ActionView attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer # Vendors the full, link and white list sanitizers. - # Provided strictly for compabitility and can be removed in Rails 5. + # Provided strictly for compatibility and can be removed in Rails 5. def sanitizer_vendor Rails::Html::Sanitizer end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a87c223a71..2562504896 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -22,9 +22,10 @@ module ActionView TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set - PRE_CONTENT_STRINGS = { - :textarea => "\n" - } + PRE_CONTENT_STRINGS = Hash.new { "".freeze } + PRE_CONTENT_STRINGS[:textarea] = "\n" + PRE_CONTENT_STRINGS["textarea"] = "\n" + # Returns an empty HTML tag of type +name+ which by default is XHTML # compliant. Set +open+ to true to create an open tag compatible @@ -143,24 +144,30 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options content = ERB::Util.unwrapped_html_escape(content) if escape - "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe end def tag_options(options, escape = true) return if options.blank? - attrs = [] + output = "" + sep = " ".freeze options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| - attrs << prefix_tag_option(key, k, v, escape) + output << sep + output << prefix_tag_option(key, k, v, escape) end elsif BOOLEAN_ATTRIBUTES.include?(key) - attrs << boolean_tag_option(key) if value + if value + output << sep + output << boolean_tag_option(key) + end elsif !value.nil? - attrs << tag_option(key, value, escape) + output << sep + output << tag_option(key, value, escape) end end - " #{attrs * ' '}" unless attrs.empty? + output unless output.empty? end def prefix_tag_option(prefix, key, value, escape) @@ -177,7 +184,7 @@ module ActionView def tag_option(key, value, escape) if value.is_a?(Array) - value = escape ? safe_join(value, " ") : value.join(" ") + value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) else value = escape ? ERB::Util.unwrapped_html_escape(value) : value end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index acc6443a96..d57f26ba4f 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -120,7 +120,12 @@ module ActionView def select_content_tag(option_tags, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) - options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options) + + if placeholder_required?(html_options) + raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false + options[:include_blank] ||= true unless options[:prompt] + end + value = options.fetch(:selected) { value(object) } select = content_tag("select", add_options(option_tags, options, value), html_options) @@ -131,8 +136,9 @@ module ActionView end end - def select_not_required?(html_options) - !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1 + def placeholder_required?(html_options) + # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required + html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1 end def add_options(option_tags, options, value = nil) diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index c216d4401f..432693bc23 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -206,6 +206,11 @@ module ActionView # +plural+ is supplied, it will use that when count is > 1, otherwise # it will use the Inflector to determine the plural form. # + # If passed an optional +locale:+ parameter, the word will be pluralized + # using rules defined for that language (you must define your own + # inflection rules for languages other than English). See + # ActiveSupport::Inflector.pluralize + # # pluralize(1, 'person') # # => 1 person # @@ -217,11 +222,14 @@ module ActionView # # pluralize(0, 'person') # # => 0 people - def pluralize(count, singular, plural = nil) + # + # pluralize(2, 'Person', locale: :de) + # # => 2 Personen + def pluralize(count, singular, plural = nil, locale: nil) word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else - plural || singular.pluralize + plural || singular.pluralize(locale) end "#{count || 0} #{word}" @@ -242,12 +250,15 @@ module ActionView # # word_wrap('Once upon a time', line_width: 1) # # => Once\nupon\na\ntime - def word_wrap(text, options = {}) - line_width = options.fetch(:line_width, 80) - + # + # You can also specify a custom +break_sequence+ ("\n" by default) + # + # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n") + # # => Once\r\nupon\r\na\r\ntime + def word_wrap(text, line_width: 80, break_sequence: "\n") text.split("\n").collect! do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line - end * "\n" + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line + end * break_sequence end # Returns +text+ transformed into HTML using simple formatting rules. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index afb1265ad9..d676a0a931 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -184,9 +184,9 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - html_options['href'] ||= url + html_options["href".freeze] ||= url - content_tag(:a, name || url, html_options, &block) + content_tag("a".freeze, name || url, html_options, &block) end # Generates a form containing a single button that submits to the URL created @@ -468,9 +468,10 @@ module ActionView }.compact extras = extras.empty? ? '' : '?' + extras.join('&') - html_options["href"] = "mailto:#{email_address}#{extras}" + encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") + html_options["href"] = "mailto:#{encoded_email_address}#{extras}" - content_tag(:a, name || email_address, html_options, &block) + content_tag("a".freeze, name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 1fc609f2cd..9d636c8c9e 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -315,25 +315,16 @@ module ActionView name_clause end - if self._layout_conditions.empty? - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout + if _conditional_layout? #{layout_definition} + else + #{name_clause} end - private :_layout - RUBY - else - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if _conditional_layout? - #{layout_definition} - else - #{name_clause} - end - end - private :_layout - RUBY - end + end + private :_layout + RUBY end private diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 4452dcfed5..f4a10aa393 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -6,10 +6,11 @@ require 'action_view/template/resolver' module ActionView # = Action View Lookup Context # - # LookupContext is the object responsible to hold all information required to lookup - # templates, i.e. view paths and details. The LookupContext is also responsible to - # generate a key, given to view paths, used in the resolver cache lookup. Since - # this key is generated just once during the request, it speeds up all cache accesses. + # <tt>LookupContext</tt> is the object responsible for holding all information + # required for looking up templates, i.e. view paths and details. + # <tt>LookupContext</tt> is also responsible for generating a key, given to + # view paths, used in the resolver cache lookup. Since this key is generated + # only once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: attr_accessor :prefixes, :rendered_format @@ -172,13 +173,13 @@ module ActionView # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: prefixes = prefixes.presence - parts = name.to_s.split('/') + parts = name.to_s.split('/'.freeze) parts.shift if parts.first.empty? name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join('/') + parts = parts.join('/'.freeze) prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -203,7 +204,7 @@ module ActionView # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*" + values.concat(default_formats) if values.delete "*/*".freeze if values == [:js] values << :html @html_fallback_for_js = true diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb index 91ee2ea8f5..7a88f6bc50 100644 --- a/actionview/lib/action_view/path_set.rb +++ b/actionview/lib/action_view/path_set.rb @@ -61,6 +61,15 @@ module ActionView #:nodoc: find_all(path, prefixes, *args).any? end + def find_all_with_query(query) # :nodoc: + paths.each do |resolver| + templates = resolver.find_all_with_query(query) + return templates unless templates.empty? + end + + [] + end + private def typecast(paths) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 9a26cba574..e829d86c99 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -42,14 +42,24 @@ module ActionView end end + initializer "action_view.per_request_digest_cache" do |app| + ActiveSupport.on_load(:action_view) do + if app.config.consider_all_requests_local + app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry + end + end + end + initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) end end - rake_tasks do - load "action_view/tasks/dependencies.rake" + rake_tasks do |app| + unless app.config.api_only + load "action_view/tasks/dependencies.rake" + end end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index b751bca31e..1f9e960488 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -348,8 +348,6 @@ module ActionView content end - private - # Sets up instance variables needed for rendering a partial. This method # finds the options and details and extracts them. The method also contains # logic that handles the type of object passed in as the partial. @@ -522,7 +520,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin - base = path[-1] == "/" ? "" : File.basename(path) + base = path[-1] == "/".freeze ? "".freeze : File.basename(path) raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ $1.to_sym end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index c8268e226e..1147963882 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -51,7 +51,7 @@ module ActionView end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key)) + key = @view.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/rendering.rb b/actionview/lib/action_view/rendering.rb index 1e8e7415d1..86a80a5421 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -59,7 +59,7 @@ module ActionView @_view_context_class ||= self.class.view_context_class end - # An instance of a view class. The default view class is ActionView::Base + # An instance of a view class. The default view class is ActionView::Base. # # The view class must have the following methods: # View.new[lookup_context, assigns, controller] @@ -103,7 +103,7 @@ module ActionView view_renderer.render(context, options) end - # Assign the rendered format to lookup context. + # Assign the rendered format to look up context. def _process_format(format, options = {}) #:nodoc: super lookup_context.formats = [format.to_sym] diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 0371db07dc..20d6b9a64c 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -92,6 +92,16 @@ module ActionView end super(options) + when ActionController::Parameters + unless options.key?(:only_path) + if options[:host].nil? + options[:only_path] = _generate_paths_by_default + else + options[:only_path] = false + end + end + + super(options) when :back _back_url when Array diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 377ceb534a..e232808dcb 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,7 @@ module ActionView @source = source @identifier = identifier @handler = handler - @cache_name = extract_resource_cache_call_name + @cache_name = extract_resource_cache_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -154,7 +154,7 @@ module ActionView # 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) - instrument("!render_template") do + instrument("!render_template".freeze) do compile!(view) view.send(method_name, locals, buffer, &block) end @@ -190,7 +190,7 @@ module ActionView end def inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier end # This method is responsible for properly setting the encoding of the @@ -337,27 +337,41 @@ module ActionView def method_name #:nodoc: @method_name ||= begin m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" - m.tr!('-', '_') + m.tr!('-'.freeze, '_'.freeze) m end end def identifier_method_name #:nodoc: - inspect.tr('^a-z_', '_') + inspect.tr('^a-z_'.freeze, '_'.freeze) end def instrument(action, &block) payload = { virtual_path: @virtual_path, identifier: @identifier } - ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block) + case action + when "!render_template".freeze + ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block) + else + ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) + end end - def extract_resource_cache_call_name - $1 if @handler.respond_to?(:resource_cache_call_pattern) && - @source =~ @handler.resource_cache_call_pattern + EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ + + def extract_resource_cache_name + if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match + match[:resource_name] + end + end + + def resource_cache_call_match + if @handler.respond_to?(:resource_cache_call_pattern) + @source.match(@handler.resource_cache_call_pattern) + end end def inferred_cache_name - @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '') + @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) end end end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 88a8570706..1f8459c24b 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -125,7 +125,7 @@ module ActionView # Returns Regexp to extract a cached resource's name from a cache call at the # first line of a template. - # The extracted cache name is expected in $1. + # The extracted cache name is captured as :resource_name. # # <% cache notification do %> # => notification # @@ -138,7 +138,14 @@ module ActionView # # <% cache notification.event do %> # => nil def resource_cache_call_pattern - /\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/ + /\A + (?:<%\#.*%>)* # optional initial comment + \s* # followed by optional spaces or newlines + <%\s*cache[\(\s] # followed by an ERB call to cache + \s* # followed by optional spaces or newlines + (?<resource_name>\w+) # capture the cache call argument as :resource_name + [\s\)] # followed by a space or close paren + /xm end private diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 955118a554..28967f40a6 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -52,6 +52,7 @@ module ActionView def initialize @data = SmallCache.new(&KEY_BLOCK) + @query_cache = SmallCache.new end # Cache the templates returned by the block @@ -70,8 +71,17 @@ module ActionView end end + def cache_query(query) # :nodoc: + if Resolver.caching? + @query_cache[query] ||= canonical_no_templates(yield) + else + yield + end + end + def clear @data.clear + @query_cache.clear end private @@ -116,6 +126,10 @@ module ActionView end end + def find_all_with_query(query) # :nodoc: + @cache.cache_query(query) { find_template_paths(File.join(@path, query)) } + end + private delegate :caching?, to: :class @@ -181,9 +195,9 @@ module ActionView def query(path, details, formats) query = build_query(path, details) - template_paths = find_template_paths query + template_paths = find_template_paths(query) - template_paths.map { |template| + template_paths.map do |template| handler, format, variant = extract_handler_and_format_and_variant(template, formats) contents = File.binread(template) @@ -193,36 +207,36 @@ module ActionView :variant => variant, :updated_at => mtime(template) ) - } + end end def find_template_paths(query) - Dir[query].reject { |filename| + Dir[query].reject do |filename| File.directory?(filename) || # deals with case-insensitive file systems. !File.fnmatch(query, filename, File::FNM_EXTGLOB) - } + end end # Helper for building query glob string based on resolver's pattern. def build_query(path, details) query = @pattern.dup - prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" - query.gsub!(/\:prefix(\/)?/, prefix) + prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/:prefix(\/)?/, prefix) partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) - query.gsub!(/\:action/, partial) + query.gsub!(/:action/, partial) details.each do |ext, variants| - query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}") end File.expand_path(query, @path) end def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) end # Returns the file mtime from the filesystem. @@ -234,7 +248,7 @@ module ActionView # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path, default_formats) - pieces = File.basename(path).split(".") + pieces = File.basename(path).split('.'.freeze) pieces.shift extension = pieces.pop diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 06810ad14d..f6b5696a13 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -24,8 +24,8 @@ module ActionView def initialize super self.class.controller_path = "" - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + @request = ActionController::TestRequest.create + @response = ActionDispatch::TestResponse.new @request.env.delete('PATH_INFO') @params = {} @@ -263,9 +263,15 @@ module ActionView end def method_missing(selector, *args) - if @controller.respond_to?(:_routes) && - ( @controller._routes.named_routes.route_defined?(selector) || - @controller._routes.mounted_helpers.method_defined?(selector) ) + begin + routes = @controller.respond_to?(:_routes) && @controller._routes + rescue + # Dont call routes, if there is an error on _routes call + end + + if routes && + ( 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/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 492f67f45d..37722013ce 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -36,9 +36,9 @@ module ActionView self.class._prefixes end - # LookupContext is the object responsible to hold all information required to lookup - # templates, i.e. view paths and details. Check ActionView::LookupContext for more - # information. + # <tt>LookupContext</tt> is the object responsible for holding all + # information required for looking up templates, i.e. view paths and + # details. Check <tt>ActionView::LookupContext</tt> for more information. def lookup_context @_lookup_context ||= ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes) diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 4635c645d0..35f520ed1c 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -147,13 +147,13 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::ParamsParser + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end diff --git a/actionview/test/actionpack/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb index a6786d9b6b..80bc665b0a 100644 --- a/actionview/test/actionpack/abstract/layouts_test.rb +++ b/actionview/test/actionpack/abstract/layouts_test.rb @@ -52,7 +52,7 @@ module AbstractControllerTests end def overwrite_skip - render :text => "Hello text!" + render plain: "Hello text!" end end @@ -371,7 +371,7 @@ module AbstractControllerTests test "layout for anonymous controller" do klass = Class.new(WithString) do def index - render :text => 'index', :layout => true + render plain: 'index', layout: true end end diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb index d09f91c1e2..e5721d6416 100644 --- a/actionview/test/actionpack/abstract/render_test.rb +++ b/actionview/test/actionpack/abstract/render_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/deprecation' module AbstractController module Testing @@ -33,7 +34,7 @@ module AbstractController end def text - render :text => "With Text" + render plain: "With Text" end def default diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 64ab125637..64bc4c41d6 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -122,14 +122,6 @@ class PrependsViewPathController < LayoutTest end end -class ParentController < LayoutTest - layout 'item' -end - -class ChildController < ParentController - layout 'layout_test', only: :hello -end - class OnlyLayoutController < LayoutTest layout 'item', :only => "hello" end @@ -157,75 +149,75 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new get :hello - assert_template :layout => "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_set_when_using_streaming_layout @controller = StreamingLayoutController.new get :hello - assert_template :hello + assert_includes @response.body, 'layout_test.erb' end def test_layout_set_when_set_in_controller @controller = HasOwnLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default @controller = HasNilLayoutSymbol.new get :hello - assert_template layout: "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default @controller = HasNilLayoutProc.new get :hello - assert_template layout: "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_only_exception_when_included @controller = OnlyLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_only_exception_when_excepted @controller = OnlyLayoutController.new get :goodbye - assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" + assert_not_includes @response.body, 'item.erb' end def test_layout_except_exception_when_included @controller = ExceptLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_except_exception_when_excepted @controller = ExceptLayoutController.new get :goodbye - assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" + assert_not_includes @response.body, 'item.erb' end def test_layout_set_when_using_render with_template_handler :mab, lambda { |template| template.source.inspect } do @controller = SetsLayoutInRenderController.new get :hello - assert_template :layout => "layouts/third_party_template_library" + assert_includes @response.body, 'layouts/third_party_template_library.mab' end end def test_layout_is_not_set_when_none_rendered @controller = RendersNoLayoutController.new get :hello - assert_template :layout => nil + assert_equal 'hello.erb', @response.body end def test_layout_is_picked_from_the_controller_instances_view_path @controller = PrependsViewPathController.new get :hello - assert_template :layout => /layouts\/alt/ + assert_includes @response.body, 'alt.erb' end def test_absolute_pathed_layout @@ -233,12 +225,6 @@ class LayoutSetInResponseTest < ActionController::TestCase get :hello assert_equal "layout_test.erb hello.erb", @response.body.strip end - - def test_respect_to_parent_layout - @controller = ChildController.new - get :goodbye - assert_template :layout => "layouts/item" - end end class SetsNonExistentLayoutFile < LayoutTest @@ -276,7 +262,7 @@ unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ @controller = LayoutSymlinkedTest.new get :hello assert_response 200 - assert_template :layout => "layouts/symlinked/symlinked_layout" + assert_includes @response.body, 'This is my layout' end end end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index d69c070ede..8d048ddbcb 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'active_model' -require 'fileutils' class ApplicationController < ActionController::Base self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") @@ -122,7 +121,7 @@ class TestController < ApplicationController # :ported: def render_hello_world_from_variable @person = "david" - render :text => "hello #{@person}" + render plain: "hello #{@person}" end # :ported: @@ -144,13 +143,13 @@ class TestController < ApplicationController # :ported: def render_text_hello_world - render :text => "hello world" + render plain: "hello world" end # :ported: def render_text_hello_world_with_layout @variable_for_layout = ", I am here!" - render :text => "hello world", :layout => true + render plain: "hello world", :layout => true end def hello_world_with_layout_false @@ -213,26 +212,26 @@ class TestController < ApplicationController # :ported: def render_custom_code - render :text => "hello world", :status => 404 + render plain: "hello world", :status => 404 end # :ported: def render_text_with_nil - render :text => nil + render plain: nil end # :ported: def render_text_with_false - render :text => false + render plain: false end def render_text_with_resource - render :text => Customer.new("David") + render plain: Customer.new("David") end # :ported: def render_nothing_with_appendix - render :text => "appended" + render plain: "appended" end # This test is testing 3 things: @@ -263,7 +262,7 @@ class TestController < ApplicationController # :ported: def blank_response - render :text => ' ' + render plain: ' ' end # :ported: @@ -295,7 +294,7 @@ class TestController < ApplicationController def hello_in_a_string @customers = [ Customer.new("david"), Customer.new("mary") ] - render :text => "How's there? " + render_to_string(:template => "test/list") + render plain: "How's there? " + render_to_string(:template => "test/list") end def accessing_params_in_template @@ -363,7 +362,7 @@ class TestController < ApplicationController def render_to_string_with_assigns @before = "i'm before the render" - render_to_string :text => "foo" + render_to_string plain: "foo" @after = "i'm after the render" render :template => "test/hello_world" end @@ -410,8 +409,8 @@ class TestController < ApplicationController # :ported: def double_render - render :text => "hello" - render :text => "world" + render plain: "hello" + render plain: "world" end def double_redirect @@ -420,13 +419,13 @@ class TestController < ApplicationController end def render_and_redirect - render :text => "hello" + render plain: "hello" redirect_to :action => "double_render" end def render_to_string_and_render - @stuff = render_to_string :text => "here is some cached stuff" - render :text => "Hi web users! #{@stuff}" + @stuff = render_to_string plain: "here is some cached stuff" + render plain: "Hi web users! #{@stuff}" end def render_to_string_with_inline_and_render @@ -455,7 +454,7 @@ class TestController < ApplicationController # :addressed: def render_text_with_assigns @hello = "world" - render :text => "foo" + render plain: "foo" end def render_with_assigns_option @@ -468,7 +467,7 @@ class TestController < ApplicationController def render_content_type_from_body response.content_type = Mime::RSS - render :text => "hello world!" + render body: "hello world!" end def render_using_layout_around_block @@ -679,33 +678,24 @@ class RenderTest < ActionController::TestCase ActionView::Base.logger = nil end - def case_sensitive_file_system? - fname = '.case_sensitive_file_system_test' - FileUtils.touch(fname) - !File.exist?(fname.upcase) - ensure - FileUtils.rm_f(fname) - end - # :ported: def test_simple_show get :hello_world assert_response 200 assert_response :success - assert_template "test/hello_world" assert_equal "<html>Hello world!</html>", @response.body end # :ported: def test_renders_default_template_for_missing_action get :'hyphen-ated' - assert_template 'test/hyphen-ated' + assert_equal "hyphen-ated.erb", @response.body end # :ported: def test_render get :render_hello_world - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end def test_line_offset @@ -721,20 +711,18 @@ class RenderTest < ActionController::TestCase # :ported: compatibility def test_render_with_forward_slash get :render_hello_world_with_forward_slash - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end # :ported: def test_render_in_top_directory get :render_template_in_top_directory - assert_template "shared" assert_equal "Elastica", @response.body end # :ported: def test_render_in_top_directory_with_slash get :render_template_in_top_directory_with_slash - assert_template "shared" assert_equal "Elastica", @response.body end @@ -752,19 +740,12 @@ class RenderTest < ActionController::TestCase # :ported: def test_render_action get :render_action_hello_world - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end def test_render_action_upcased - action = :render_action_upcased_hello_world - - if case_sensitive_file_system? - assert_raise ActionView::MissingTemplate do - get action - end - else - get action - assert_template 'test/Hello_world' + assert_raise ActionView::MissingTemplate do + get :render_action_upcased_hello_world end end @@ -772,13 +753,12 @@ class RenderTest < ActionController::TestCase def test_render_action_hello_world_as_string get :render_action_hello_world_as_string assert_equal "Hello world!", @response.body - assert_template "test/hello_world" end # :ported: def test_render_action_with_symbol get :render_action_hello_world_with_symbol - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end # :ported: @@ -790,7 +770,7 @@ class RenderTest < ActionController::TestCase # :ported: def test_do_with_render_text_and_layout get :render_text_hello_world_with_layout - assert_equal "<html>hello world, I am here!</html>", @response.body + assert_equal "{{hello world, I am here!}}\n", @response.body end # :ported: @@ -966,7 +946,7 @@ class RenderTest < ActionController::TestCase def test_render_to_string_inline get :render_to_string_with_inline_and_render - assert_template "test/hello_world" + assert_equal 'Hello world!', @response.body end # :ported: @@ -1051,8 +1031,8 @@ class RenderTest < ActionController::TestCase def test_render_to_string_doesnt_break_assigns get :render_to_string_with_assigns - assert_equal "i'm before the render", assigns(:before) - assert_equal "i'm after the render", assigns(:after) + assert_equal "i'm before the render", @controller.instance_variable_get(:@before) + assert_equal "i'm after the render", @controller.instance_variable_get(:@after) end def test_bad_render_to_string_still_throws_exception @@ -1061,8 +1041,8 @@ class RenderTest < ActionController::TestCase def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns assert_nothing_raised { get :render_to_string_with_caught_exception } - assert_equal "i'm before the render", assigns(:before) - assert_equal "i'm after the render", assigns(:after) + assert_equal "i'm before the render", @controller.instance_variable_get(:@before) + assert_equal "i'm after the render", @controller.instance_variable_get(:@after) end def test_accessing_params_in_template_with_layout @@ -1123,7 +1103,7 @@ class RenderTest < ActionController::TestCase # :addressed: def test_render_text_with_assigns get :render_text_with_assigns - assert_equal "world", assigns["hello"] + assert_equal "world", @controller.instance_variable_get(:@hello) end def test_render_text_with_assigns_option @@ -1189,22 +1169,22 @@ class RenderTest < ActionController::TestCase def test_render_to_string_partial get :render_to_string_with_partial - assert_equal "only partial", assigns(:partial_only) - assert_equal "Hello: david", assigns(:partial_with_locals) + assert_equal "only partial", @controller.instance_variable_get(:@partial_only) + assert_equal "Hello: david", @controller.instance_variable_get(:@partial_with_locals) assert_equal "text/html", @response.content_type end def test_render_to_string_with_template_and_html_partial get :render_to_string_with_template_and_html_partial - assert_equal "**only partial**\n", assigns(:text) - assert_equal "<strong>only partial</strong>\n", assigns(:html) + assert_equal "**only partial**\n", @controller.instance_variable_get(:@text) + assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html) assert_equal "<strong>only html partial</strong>\n", @response.body assert_equal "text/html", @response.content_type end def test_render_to_string_and_render_with_different_formats get :render_to_string_and_render_with_different_formats - assert_equal "<strong>only partial</strong>\n", assigns(:html) + assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html) assert_equal "**only partial**\n", @response.body assert_equal "text/plain", @response.content_type end @@ -1228,21 +1208,18 @@ class RenderTest < ActionController::TestCase def test_partial_with_form_builder get :partial_with_form_builder - assert_match(/<label/, @response.body) - assert_template('test/_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_partial_with_form_builder_subclass get :partial_with_form_builder_subclass - assert_match(/<label/, @response.body) - assert_template('test/_labelling_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_nested_partial_with_form_builder @controller = Fun::GamesController.new get :nested_partial_with_form_builder - assert_match(/<label/, @response.body) - assert_template('fun/games/_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_namespaced_object_partial @@ -1286,48 +1263,29 @@ class RenderTest < ActionController::TestCase assert_equal "Bonjour: davidBonjour: mary", @response.body end - def test_locals_option_to_assert_template_is_not_supported - get :partial_collection_with_locals - - warning_buffer = StringIO.new - $stderr = warning_buffer - - assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' } - assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string - ensure - $stderr = STDERR - end - def test_partial_collection_with_spacer get :partial_collection_with_spacer assert_equal "Hello: davidonly partialHello: mary", @response.body - assert_template :partial => '_customer' end def test_partial_collection_with_spacer_which_uses_render get :partial_collection_with_spacer_which_uses_render assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body - assert_template :partial => '_customer' end def test_partial_collection_shorthand_with_locals get :partial_collection_shorthand_with_locals assert_equal "Bonjour: davidBonjour: mary", @response.body - assert_template :partial => 'customers/_customer', :count => 2 - assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0 end def test_partial_collection_shorthand_with_different_types_of_records get :partial_collection_shorthand_with_different_types_of_records assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body - assert_template :partial => 'good_customers/_good_customer', :count => 3 - assert_template :partial => 'bad_customers/_bad_customer', :count => 3 end def test_empty_partial_collection get :empty_partial_collection assert_equal " ", @response.body - assert_template :partial => false end def test_partial_with_hash_object diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index 7fba9ff8ff..e99659c802 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -23,8 +23,8 @@ class ViewLoadPathsTest < ActionController::TestCase end def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + @request = ActionController::TestRequest.create + @response = ActionDispatch::TestResponse.new @controller = TestController.new @paths = TestController.view_paths 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 409370104d..9772ebb39e 100644 --- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -52,43 +52,37 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase def test_rendering_partial_with_has_many_and_belongs_to_association get :render_with_has_many_and_belongs_to_association - assert_template 'projects/_project' - assert_equal assigns(:developer).projects.map(&:name).join, @response.body + assert_equal Developer.find(1).projects.map(&:name).join, @response.body end def test_rendering_partial_with_has_many_association get :render_with_has_many_association - assert_template 'replies/_reply' assert_equal 'Birdman is better!', @response.body end def test_rendering_partial_with_scope get :render_with_scope - assert_template 'replies/_reply' assert_equal 'Birdman is better!Nuh uh!', @response.body end def test_render_with_record get :render_with_record - assert_template 'developers/_developer' assert_equal 'David', @response.body end def test_render_with_record_collection get :render_with_record_collection - assert_template 'developers/_developer' assert_equal 'DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis', @response.body end def test_render_with_record_collection_and_spacer_template get :render_with_record_collection_and_spacer_template - assert_equal assigns(:developer).projects.map(&:name).join('only partial'), @response.body + assert_equal Developer.find(1).projects.map(&:name).join('only partial'), @response.body end def test_rendering_partial_with_has_one_association mascot = Company.find(1).mascot get :render_with_has_one_association - assert_template 'mascots/_mascot' assert_equal mascot.name, @response.body end end @@ -130,13 +124,11 @@ class RenderPartialWithRecordIdentificationAndNestedControllersTest < ActiveReco def test_render_with_record_in_nested_controller get :render_with_record_in_nested_controller - assert_template %r{\Afun/games/_game\Z} assert_equal "Fun Pong\n", @response.body end def test_render_with_record_collection_in_nested_controller get :render_with_record_collection_in_nested_controller - assert_template %r{\Afun/games/_game\Z} assert_equal "Fun Pong\nFun Tank\n", @response.body end end @@ -149,7 +141,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_in_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Pong\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -160,7 +151,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_collection_in_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Pong\nJust Tank\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -172,13 +162,11 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersTest < Acti def test_render_with_record_in_deeper_nested_controller get :render_with_record_in_deeper_nested_controller - assert_template %r{\Afun/serious/games/_game\Z} assert_equal "Serious Chess\n", @response.body end def test_render_with_record_collection_in_deeper_nested_controller get :render_with_record_collection_in_deeper_nested_controller - assert_template %r{\Afun/serious/games/_game\Z} assert_equal "Serious Chess\nSerious Sudoku\nSerious Solitaire\n", @response.body end end @@ -191,7 +179,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_in_deeper_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Chess\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -202,7 +189,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_collection_in_deeper_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Chess\nJust Sudoku\nJust Solitaire\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config diff --git a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb index e69de29bb2..60b81525b5 100644 --- a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb +++ b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb @@ -0,0 +1 @@ +alt.erb diff --git a/actionview/test/fixtures/actionpack/layouts/standard.text.erb b/actionview/test/fixtures/actionpack/layouts/standard.text.erb new file mode 100644 index 0000000000..a58afb1aa2 --- /dev/null +++ b/actionview/test/fixtures/actionpack/layouts/standard.text.erb @@ -0,0 +1 @@ +{{<%= yield %><%= @variable_for_layout %>}} diff --git a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb index cd0875583a..28dbe94ee1 100644 --- a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb +++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb @@ -1 +1 @@ -Hello world! +hyphen-ated.erb
\ No newline at end of file diff --git a/actionview/test/fixtures/digestor/events/_completed.html.erb b/actionview/test/fixtures/digestor/events/_completed.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/events/_completed.html.erb diff --git a/actionview/test/fixtures/digestor/events/index.html.erb b/actionview/test/fixtures/digestor/events/index.html.erb new file mode 100644 index 0000000000..bc45e41bcb --- /dev/null +++ b/actionview/test/fixtures/digestor/events/index.html.erb @@ -0,0 +1 @@ +<% # Template Dependency: events/* %>
\ No newline at end of file diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 6e6ce20924..01fc66bed6 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -310,6 +310,11 @@ class AssetTagHelperTest < ActionView::TestCase AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_asset_path_tag_raises_an_error_for_nil_source + e = assert_raise(ArgumentError) { asset_path(nil) } + assert_equal("nil is not a valid asset source", e.message) + end + def test_asset_path_tag_to_not_create_duplicate_slashes @controller.config.asset_host = "host/" assert_dom_equal('http://host/foo', asset_path("foo")) diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index c2b8439df3..dde757b5a2 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'fileutils' +require 'action_view/dependency_tracker' class FixtureTemplate attr_reader :source, :handler @@ -15,12 +16,13 @@ end class FixtureFinder FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" - attr_reader :details + attr_reader :details, :view_paths attr_accessor :formats attr_accessor :variants def initialize @details = {} + @view_paths = ActionView::PathSet.new(['digestor']) @formats = [] @variants = [] end @@ -75,6 +77,34 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_explicit_dependency_wildcard + assert_digest_difference("events/index") do + change_template("events/_completed") + end + end + + def test_explicit_dependency_wildcard_picks_up_added_file + old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false + + assert_digest_difference("events/index") do + add_template("events/_uncompleted") + end + ensure + remove_template("events/_uncompleted") + ActionView::Resolver.caching = old_caching + end + + def test_explicit_dependency_wildcard_picks_up_removed_file + old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false + add_template("events/_subscribers_changed") + + assert_digest_difference("events/index") do + remove_template("events/_subscribers_changed") + end + ensure + ActionView::Resolver.caching = old_caching + end + def test_second_level_dependency assert_digest_difference("messages/show") do change_template("comments/_comments") @@ -111,6 +141,18 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_logging_of_missing_template_for_dependencies + assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" 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 + nested_dependencies("messages/something_missing") + end + end + def test_nested_template_directory assert_digest_difference("messages/show") do change_template("messages/actions/_move") @@ -207,7 +249,7 @@ class TemplateDigestorTest < ActionView::TestCase end def test_variants - assert_digest_difference("messages/new", false, variants: [:iphone]) do + assert_digest_difference("messages/new", variants: [:iphone]) do change_template("messages/new", :iphone) change_template("messages/_header", :iphone) end @@ -227,16 +269,6 @@ class TemplateDigestorTest < ActionView::TestCase assert_not_equal digest_phone, digest_fridge_phone end - def test_cache_template_loading - resolver_before = ActionView::Resolver.caching - ActionView::Resolver.caching = false - assert_digest_difference("messages/edit", true) do - change_template("comments/_comment") - end - ensure - ActionView::Resolver.caching = resolver_before - end - def test_digest_cache_cleanup_with_recursion first_digest = digest("level/_recursion") second_digest = digest("level/_recursion") @@ -279,9 +311,9 @@ class TemplateDigestorTest < ActionView::TestCase end end - def assert_digest_difference(template_name, persistent = false, options = {}) + def assert_digest_difference(template_name, options = {}) previous_digest = digest(template_name, options) - ActionView::Digestor.cache.clear unless persistent + ActionView::Digestor.cache.clear yield @@ -298,6 +330,14 @@ class TemplateDigestorTest < ActionView::TestCase ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) end + def dependencies(template_name) + ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies + end + + def nested_dependencies(template_name) + ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies + end + def finder @finder ||= FixtureFinder.new end @@ -309,4 +349,9 @@ class TemplateDigestorTest < ActionView::TestCase f.write "\nTHIS WAS CHANGED!" end end + alias_method :add_template, :change_template + + def remove_template(template_name) + File.delete("digestor/#{template_name}.html.erb") + end end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index b8cb5bd746..37f2d7cad6 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1577,7 +1577,7 @@ class FormHelperTest < ActionView::TestCase "<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' type='submit' value='Create post' />" + + "<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 @@ -1854,7 +1854,7 @@ class FormHelperTest < ActionView::TestCase 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='commit' type='submit' value='Edit post' />" + "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end assert_dom_equal expected, output_buffer @@ -1875,7 +1875,7 @@ class FormHelperTest < ActionView::TestCase "<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' type='submit' />" + "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end assert_dom_equal expected, output_buffer @@ -2083,7 +2083,7 @@ class FormHelperTest < ActionView::TestCase 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>" + - "<input name='commit' type='submit' value='Create post' />" + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end assert_dom_equal expected, output_buffer @@ -2101,7 +2101,7 @@ class FormHelperTest < ActionView::TestCase 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>" + - "<input name='commit' type='submit' value='Create post' />" + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end assert_dom_equal expected, output_buffer @@ -2226,7 +2226,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts', 'new_post', 'new_post') do - "<input name='commit' type='submit' value='Create Post' />" + "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />" end assert_dom_equal expected, output_buffer @@ -2240,7 +2240,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do - "<input name='commit' type='submit' value='Confirm Post changes' />" + "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />" end assert_dom_equal expected, output_buffer @@ -2254,7 +2254,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form do - "<input name='commit' class='extra' type='submit' value='Save changes' />" + "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />" end assert_dom_equal expected, output_buffer @@ -2268,7 +2268,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do - "<input name='commit' type='submit' value='Update your Post' />" + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" end assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index d25fa3706f..d7daba8bf3 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -645,6 +645,13 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_include_blank_false_and_required + @post = Post.new + @post.category = "<mus>" + e = assert_raises(ArgumentError) { select("post", "category", %w( abe <mus> hest), { include_blank: false }, required: 'required') } + assert_match(/include_blank cannot be false for a required field./, e.message) + end + def test_select_with_blank_as_string @post = Post.new @post.category = "<mus>" diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index cad1c82309..a9d9562580 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -433,6 +433,44 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_empty_submit_tag + assert_dom_equal( + %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />), + submit_tag("Save") + ) + end + + def test_empty_submit_tag_with_opt_out + ActionView::Base.automatically_disable_submit_tag = false + assert_dom_equal( + %(<input name='commit' type="submit" value="Save" />), + submit_tag("Save") + ) + ensure + ActionView::Base.automatically_disable_submit_tag = true + end + + def test_data_disable_with_string + assert_dom_equal( + %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) + ) + end + + def test_data_disable_with_boolean + assert_dom_equal( + %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" }) + ) + end + + def test_data_hash_disable_with_boolean + assert_dom_equal( + %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } }) + ) + end + def test_submit_tag_with_no_onclick_options assert_dom_equal( %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />), @@ -442,7 +480,7 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag_with_confirmation assert_dom_equal( - %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />), + %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" data-disable-with="Save" />), submit_tag("Save", :data => { :confirm => "Are you sure?" }) ) end @@ -510,6 +548,13 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_button_tag_with_data_disable_with_option + assert_dom_equal( + %(<button name="button" type="submit" data-disable-with="Please wait...">Checkout</button>), + button_tag("Checkout", data: { disable_with: "Please wait..." }) + ) + end + def test_image_submit_tag_with_confirmation assert_dom_equal( %(<input alt="Save" type="image" src="/images/save.gif" data-confirm="Are you sure?" />), diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb index 4b7b653916..bfc5d04bed 100644 --- a/actionview/test/template/record_tag_helper_test.rb +++ b/actionview/test/template/record_tag_helper_test.rb @@ -2,7 +2,7 @@ require 'abstract_unit' class RecordTagPost extend ActiveModel::Naming - include ActiveModel::Conversion + attr_accessor :id, :body def initialize @@ -14,7 +14,6 @@ class RecordTagPost end class RecordTagHelperTest < ActionView::TestCase - include RenderERBUtils tests ActionView::Helpers::RecordTagHelper diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 27bbb9b6c1..9c2c9507b7 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -7,7 +7,10 @@ end module RenderTestCases def setup_view(paths) @assigns = { :secret => 'in the sauce' } - @view = ActionView::Base.new(paths, @assigns) + @view = Class.new(ActionView::Base) do + def view_cache_dependencies; end + end.new(paths, @assigns) + @controller_view = TestController.new.view_context # Reload and register danish language for testing @@ -616,7 +619,7 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest test "with custom key" do customer = Customer.new("david") - key = ActionController::Base.new.fragment_cache_key([customer, 'key']) + key = cache_key([customer, 'key'], "test/_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Hello') @@ -626,7 +629,7 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest test "automatic caching with inferred cache name" do customer = CachedCustomer.new("david") - key = ActionController::Base.new.fragment_cache_key(customer) + key = cache_key(customer, "test/_cached_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') @@ -636,11 +639,17 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest test "automatic caching with as name" do customer = CachedCustomer.new("david") - key = ActionController::Base.new.fragment_cache_key(customer) + key = cache_key(customer, "test/_cached_customer_as") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') assert_equal "Cached", @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer) end + + private + def cache_key(names, virtual_path) + digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] + @view.fragment_cache_key([ *Array(names), digest ]) + end end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index aae6a9aa09..d3b51cd629 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -190,6 +190,38 @@ class TestERBTemplate < ActiveSupport::TestCase assert_match(/\xFC/, e.message) end + def test_not_eligible_for_collection_caching_without_cache_call + [ + "<%= 'Hello' %>", + "<% cache_customer = 42 %>", + "<% cache customer.name do %><% end %>", + "<% my_cache customer do %><% end %>" + ].each do |body| + template = new_template(body, virtual_path: "test/foo/_customer") + assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching" + end + end + + def test_eligible_for_collection_caching_with_cache_call_or_explicit + [ + "<% cache customer do %><% end %>", + "<% cache(customer) do %><% end %>", + "<% cache( customer) do %><% end %>", + "<% cache( customer ) do %><% end %>", + "<%cache customer do %><% end %>", + "<% cache customer do %><% end %>", + " <% cache customer do %>\n<% end %>\n", + "<%# comment %><% cache customer do %><% end %>", + "<%# comment %>\n<% cache customer do %><% end %>", + "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>", + "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>", + "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>" + ].each do |body| + template = new_template(body, virtual_path: "test/foo/_customer") + assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching" + end + end + def with_external_encoding(encoding) old = Encoding.default_external Encoding::Converter.new old, encoding if old != encoding diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index c6cc47fb4f..80fbaa8392 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -209,7 +209,7 @@ module ActionView end test "is able to use routes" do - controller.request.assign_parameters(@routes, 'foo', 'index') + controller.request.assign_parameters(@routes, 'foo', 'index', {}, '/foo', []) assert_equal '/foo', url_for assert_equal '/bar', url_for(:controller => 'bar') end @@ -306,64 +306,6 @@ module ActionView end end - class RenderTemplateTest < ActionView::TestCase - test "supports specifying templates with a Regexp" do - controller.controller_path = "fun" - render(:template => "fun/games/hello_world") - assert_template %r{\Afun/games/hello_world\Z} - end - - test "supports specifying partials" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - assert_template :partial => "_partial_for_use_in_layout" - end - - test "supports specifying locals (passing)" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" } - end - - test "supports specifying locals (failing)" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - e = assert_raise ActiveSupport::TestCase::Assertion do - assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" } - end - assert_match(/Somebody Else.*David/m, e.message) - end - - test 'supports different locals on the same partial' do - controller.controller_path = "test" - render(:template => "test/render_two_partials") - assert_template partial: '_partial', locals: { 'first' => '1' } - assert_template partial: '_partial', locals: { 'second' => '2' } - end - - test 'raises descriptive error message when template was not rendered' do - controller.controller_path = "test" - render(template: "test/hello_world_with_partial") - e = assert_raise ActiveSupport::TestCase::Assertion do - assert_template partial: 'i_was_never_rendered', locals: { 'did_not' => 'happen' } - end - assert_match "i_was_never_rendered to be rendered but it was not.", e.message - assert_match 'Expected ["/test/partial"] to include "i_was_never_rendered"', e.message - end - - test 'specifying locals works when the partial is inside a directory with underline prefix' do - controller.controller_path = "test" - render(template: 'test/render_partial_inside_directory') - assert_template partial: 'test/_directory/_partial_with_locales', locals: { 'name' => 'Jane' } - end - - test 'specifying locals works when the partial is inside a directory without underline prefix' do - controller.controller_path = "test" - render(template: 'test/render_partial_inside_directory') - assert_template partial: 'test/_directory/partial_with_locales', locals: { 'name' => 'Jane' } - end - end - module AHelperWithInitialize def initialize(*) super diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index f1b84c4786..fae1965ffa 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -366,6 +366,10 @@ class TextHelperTest < ActionView::TestCase assert_equal options, passed_options end + def test_word_wrap_with_custom_break_sequence + assert_equal("1234567890\r\n1234567890\r\n1234567890", word_wrap("1234567890 " * 3, line_width: 2, break_sequence: "\r\n")) + end + def test_pluralization assert_equal("1 count", pluralize(1, "count")) assert_equal("2 counts", pluralize(2, "count")) @@ -383,6 +387,18 @@ class TextHelperTest < ActionView::TestCase assert_equal("12 berries", pluralize(12, "berry")) end + def test_pluralization_with_locale + ActiveSupport::Inflector.inflections(:de) do |inflect| + inflect.plural(/(person)$/i, '\1en') + inflect.singular(/(person)en$/i, '\1') + end + + assert_equal("2 People", pluralize(2, "Person", locale: :en)) + assert_equal("2 Personen", pluralize(2, "Person", locale: :de)) + + ActiveSupport::Inflector.inflections(:de).clear + end + def test_cycle_class value = Cycle.new("one", 2, "3") assert_equal("one", value.to_s) diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 0e35c67516..416d30938a 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -500,6 +500,13 @@ class UrlHelperTest < ActiveSupport::TestCase mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin") end + 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") + ) + end + def test_mail_with_options assert_dom_equal( %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&bcc=bccaddress%40example.com&body=This%20is%20the%20body%20of%20the%20message.&subject=This%20is%20an%20example%20email&reply-to=foo%40bar.com">My email</a>}, diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index aba386c18e..637096e935 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,9 +1,26 @@ +* Implement `provider_job_id` for `queue_classic` adapter. This requires the + latest, currently unreleased, version of queue_classic. + + *Yves Senn* + +* `assert_enqueued_with` and `assert_performed_with` now returns the matched + job instance for further assertions. + + *Jean Boussier* + +* Include I18n.locale into job serialization/deserialization and use it around + `perform`. + + Fixes #20799. + + *Johannes Opper* + * Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to `ActiveJob::Base` as `provider_job_id`. Fixes #18821. - *Kevin Deisz* And *Jeroen van Baarsen* + *Kevin Deisz*, *Jeroen van Baarsen* * `assert_enqueued_jobs` and `assert_performed_jobs` in block form use the given number as expected value. This makes the error message much easier to diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index cdb37879b6..8e462bfe5d 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -16,13 +16,13 @@ module ActiveJob end end - # Raised when an unsupported argument type is being set as job argument. We + # Raised when an unsupported argument type is set as a job argument. We # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum and object that can be represented as GlobalIDs (ex: Active Record). - # Also raised if you set the key for a Hash something else than a string or - # a symbol. - class SerializationError < ArgumentError - end + # Bignum and objects that can be represented as GlobalIDs (ex: Active Record). + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a Global ID - such as an unpersisted Active Record model. + class SerializationError < ArgumentError; end module Arguments extend self @@ -59,7 +59,7 @@ module ActiveJob when *TYPE_WHITELIST argument when GlobalID::Identification - { GLOBALID_KEY => argument.to_global_id.to_s } + convert_to_global_id_hash(argument) when Array argument.map { |arg| serialize_argument(arg) } when ActiveSupport::HashWithIndifferentAccess @@ -147,5 +147,12 @@ module ActiveJob end end end + + def convert_to_global_id_hash(argument) + { GLOBALID_KEY => argument.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{argument.class} " \ + "without an id. (Maybe you forgot to call save?)" + end end end diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index fd49b3fda5..5d7c4cfb91 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -5,6 +5,7 @@ require 'active_job/enqueuing' require 'active_job/execution' require 'active_job/callbacks' require 'active_job/logging' +require 'active_job/translation' module ActiveJob #:nodoc: # = Active Job @@ -60,6 +61,7 @@ module ActiveJob #:nodoc: include Execution include Callbacks include Logging + include Translation ActiveSupport.run_load_hooks(:active_job, self) end diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 0528572cd0..eac7279309 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -20,6 +20,9 @@ module ActiveJob # ID optionally provided by adapter attr_accessor :provider_job_id + + # I18n.locale to be used during the job. + attr_accessor :locale end # These methods will be included into any Active Job object, adding @@ -68,7 +71,8 @@ module ActiveJob 'job_class' => self.class.name, 'job_id' => job_id, 'queue_name' => queue_name, - 'arguments' => serialize_arguments(arguments) + 'arguments' => serialize_arguments(arguments), + 'locale' => I18n.locale } end @@ -96,6 +100,7 @@ module ActiveJob self.job_id = job_data['job_id'] self.queue_name = job_data['queue_name'] self.serialized_arguments = job_data['arguments'] + self.locale = job_data['locale'] || I18n.locale end private diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 054a5d93b4..457015b741 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -15,6 +15,8 @@ module ActiveJob # Includes the setter method for changing the active queue adapter. module ClassMethods + # Returns the backend queue provider. The default queue adapter + # is the +:inline+ queue. See QueueAdapters for more information. def queue_adapter _queue_adapter end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index bd69e525bb..e8ceabaeba 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -15,18 +15,18 @@ module ActiveJob # # === Backends Features # - # | | Async | Queues | Delayed | Priorities | Timeout | Retries | - # |-------------------|-------|--------|-----------|------------|---------|---------| - # | Backburner | Yes | Yes | Yes | Yes | Job | Global | - # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | - # | Qu | Yes | Yes | No | No | No | Global | - # | Que | Yes | Yes | Yes | Job | No | Job | - # | queue_classic | Yes | Yes | No* | No | No | No | - # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | - # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | - # | Sneakers | Yes | Yes | No | Queue | Queue | No | - # | Sucker Punch | Yes | Yes | No | No | No | No | - # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | + # | | Async | Queues | Delayed | Priorities | Timeout | Retries | + # |-------------------|-------|--------|------------|------------|---------|---------| + # | Backburner | Yes | Yes | Yes | Yes | Job | Global | + # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | + # | Qu | Yes | Yes | No | No | No | Global | + # | Que | Yes | Yes | Yes | Job | No | Job | + # | queue_classic | Yes | Yes | Yes* | No | No | No | + # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | + # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | + # | Sneakers | Yes | Yes | No | Queue | Queue | No | + # | Sucker Punch | Yes | Yes | No | No | No | No | + # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | # # ==== Async # @@ -50,9 +50,8 @@ module ActiveJob # N/A: The adapter does not support queueing. # # NOTE: - # queue_classic does not support job scheduling. - # However, you can use the queue_classic-later gem. - # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic supports job scheduling since version 3.1. + # For older versions you can use the queue_classic-later gem. # # ==== Priorities # @@ -116,6 +115,10 @@ module ActiveJob private_constant :ADAPTER class << self + # Returns adapter for specified name. + # + # ActiveJob::QueueAdapters.lookup(:sidekiq) + # # => ActiveJob::QueueAdapters::SidekiqAdapter def lookup(name) const_get(name.to_s.camelize << ADAPTER) end diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb index 059754a87f..0ee41407d8 100644 --- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -18,7 +18,9 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :queue_classic class QueueClassicAdapter def enqueue(job) #:nodoc: - build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + qc_job = build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash) + qc_job end def enqueue_at(job, timestamp) #:nodoc: @@ -28,7 +30,9 @@ module ActiveJob 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \ 'You can implement this yourself or you can use the queue_classic-later gem.' end - queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + qc_job = queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash) + qc_job end # Builds a <tt>QC::Queue</tt> object to schedule jobs on. diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 9ae0345120..65786a49ff 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -39,7 +39,7 @@ module ActiveJob self.queue_name_delimiter = '_' # set default delimiter to '_' end - # Returns the name of the queue the job will be run on + # Returns the name of the queue the job will be run on. def queue_name if @queue_name.is_a?(Proc) @queue_name = self.class.queue_name_from_part(instance_exec(&@queue_name)) diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 9b307e8dc8..200d82838e 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -7,7 +7,7 @@ module ActiveJob extend ActiveSupport::Concern included do - def before_setup + def before_setup # :nodoc: test_adapter = ActiveJob::QueueAdapters::TestAdapter.new @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| @@ -24,7 +24,7 @@ module ActiveJob super end - def after_teardown + def after_teardown # :nodoc: super @old_queue_adapters.each do |(klass, adapter)| klass.queue_adapter = adapter @@ -233,10 +233,11 @@ module ActiveJob args.assert_valid_keys(:job, :args, :at, :queue) serialized_args = serialize_args_for_assertion(args) yield - matching_job = enqueued_jobs.any? do |job| + matching_job = enqueued_jobs.find do |job| serialized_args.all? { |key, value| value == job[key] } end assert matching_job, "No enqueued job found with #{args}" + instantiate_job(matching_job) ensure queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs end @@ -254,10 +255,11 @@ module ActiveJob args.assert_valid_keys(:job, :args, :at, :queue) serialized_args = serialize_args_for_assertion(args) perform_enqueued_jobs { yield } - matching_job = performed_jobs.any? do |job| + matching_job = performed_jobs.find do |job| serialized_args.all? { |key, value| value == job[key] } end assert matching_job, "No performed job found with #{args}" + instantiate_job(matching_job) ensure queue_adapter.performed_jobs = original_performed_jobs + performed_jobs end @@ -311,6 +313,13 @@ module ActiveJob end serialized_args end + + 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 end end end diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb new file mode 100644 index 0000000000..67e4cf4ab9 --- /dev/null +++ b/activejob/lib/active_job/translation.rb @@ -0,0 +1,11 @@ +module ActiveJob + module Translation #:nodoc: + extend ActiveSupport::Concern + + included do + around_perform do |job, block, _| + I18n.with_locale(job.locale, &block) + end + end + end +end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 8b9b62190f..933972a52b 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -88,6 +88,13 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_equal "Job with argument: 2", JobBuffer.last_value end + test 'raises a friendly SerializationError for records without ids' do + err = assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [Person.new(nil)] + end + assert_match 'Unable to serialize Person without an id.', err.message + end + private def assert_arguments_unchanged(*args) assert_arguments_roundtrip args diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index db22783030..229517774e 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -1,5 +1,6 @@ require 'helper' require 'jobs/gid_job' +require 'jobs/hello_job' require 'models/person' class JobSerializationTest < ActiveSupport::TestCase @@ -12,4 +13,20 @@ class JobSerializationTest < ActiveSupport::TestCase GidJob.perform_later @person assert_equal "Person with ID: 5", JobBuffer.last_value end + + test 'serialize includes current locale' do + assert_equal :en, HelloJob.new.serialize['locale'] + end + + test 'deserialize sets locale' do + job = HelloJob.new + job.deserialize 'locale' => :es + assert_equal :es, job.locale + end + + test 'deserialize sets default locale' do + job = HelloJob.new + job.deserialize({}) + assert_equal :en, job.locale + end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 04c4c446e2..8c60f037d2 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -165,6 +165,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_job_returns + job = assert_enqueued_with(job: LoggingJob) do + LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) + end + + assert_instance_of LoggingJob, job + assert_in_delta 5.minutes.from_now, job.scheduled_at, 1 + assert_equal 'default', job.queue_name + assert_equal [1, 2, 3], job.arguments + end + def test_assert_enqueued_job_failure assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_with(job: LoggingJob, queue: 'default') do @@ -397,6 +408,17 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_job_returns + job = assert_performed_with(job: NestedJob, queue: 'default') do + NestedJob.perform_later + end + + assert_instance_of NestedJob, job + assert_nil job.scheduled_at + assert_equal [], job.arguments + assert_equal 'default', job.queue_name + end + def test_assert_performed_job_failure assert_raise ActiveSupport::TestCase::Assertion do assert_performed_with(job: LoggingJob, at: Date.tomorrow.noon, queue: 'default') do diff --git a/activejob/test/cases/translation_test.rb b/activejob/test/cases/translation_test.rb new file mode 100644 index 0000000000..d5e3aaf9e3 --- /dev/null +++ b/activejob/test/cases/translation_test.rb @@ -0,0 +1,20 @@ +require 'helper' +require 'jobs/translated_hello_job' + +class TranslationTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + I18n.available_locales = [:en, :de] + @job = TranslatedHelloJob.new('Johannes') + end + + teardown do + I18n.available_locales = [:en] + end + + test 'it performs the job in the given locale' do + @job.locale = :de + @job.perform_now + assert_equal "Johannes says Guten Tag", JobBuffer.last_value + end +end diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index d345092dee..ca8047ef0b 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -57,15 +57,31 @@ class QueuingTest < ActiveSupport::TestCase end test 'should supply a provider_job_id when available for immediate jobs' do - skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que) + skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que, :queue_classic) test_job = TestJob.perform_later @id - refute test_job.provider_job_id.nil?, 'Provider job id should be set by provider' + assert test_job.provider_job_id, 'Provider job id should be set by provider' end test 'should supply a provider_job_id when available for delayed jobs' do - skip unless adapter_is?(:delayed_job, :sidekiq, :que) + skip unless adapter_is?(:delayed_job, :sidekiq, :que, :queue_classic) delayed_test_job = TestJob.set(wait: 1.minute).perform_later @id - refute delayed_test_job.provider_job_id.nil?, - 'Provider job id should by set for delayed jobs by provider' + assert delayed_test_job.provider_job_id, 'Provider job id should by set for delayed jobs by provider' + end + + test 'current locale is kept while running perform_later' do + skip if adapter_is?(:inline) + + begin + I18n.available_locales = [:en, :de] + I18n.locale = :de + + TestJob.perform_later @id + wait_for_jobs_to_finish_for(5.seconds) + assert job_executed + assert_equal 'de', job_output + ensure + I18n.available_locales = [:en] + I18n.locale = :en + end end end diff --git a/activejob/test/jobs/translated_hello_job.rb b/activejob/test/jobs/translated_hello_job.rb new file mode 100644 index 0000000000..9657cd3f54 --- /dev/null +++ b/activejob/test/jobs/translated_hello_job.rb @@ -0,0 +1,10 @@ +require_relative '../support/job_buffer' + +class TranslatedHelloJob < ActiveJob::Base + def perform(greeter = "David") + translations = { en: 'Hello', de: 'Guten Tag' } + hello = translations[I18n.locale] + + JobBuffer.add("#{greeter} says #{hello}") + end +end diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index 4988cdb33f..d9938600f2 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -57,7 +57,7 @@ module SidekiqJobsManager concurrency: 1, timeout: 1, }) - Sidekiq.poll_interval = 0.5 + Sidekiq.average_scheduled_poll_interval = 0.5 Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 begin sidekiq.run diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 09a68738ad..4ffdb8cffa 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -8,13 +8,17 @@ require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" JobsManager.current_manager.setup CODE +initializer 'i18n.rb', <<-CODE +I18n.available_locales = [:en, :de] +CODE + file 'app/jobs/test_job.rb', <<-CODE class TestJob < ActiveJob::Base queue_as :integration_tests def perform(x) File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f| - f.write x + f.write I18n.locale end end end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index 7e87ede275..39aee6d407 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -45,4 +45,8 @@ module TestCaseHelpers def job_executed Dummy::Application.root.join("tmp/#{@id}").exist? end + + def job_output + File.read Dummy::Application.root.join("tmp/#{@id}") + end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index dddfd940bb..f25c901ecc 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -3,6 +3,10 @@ *Jay Elaraj* +* Remove `ActiveModel::Serializers::Xml` from core. + + *Zachary Scott* + * Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and `ActiveModel::Dirty#[attr_name]_previous_change` to improve access to recorded changes after the model has been saved. diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 5c36b1277e..20414c1d61 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -154,8 +154,8 @@ behavior out of the box: * Making objects serializable - ActiveModel::Serialization provides a standard interface for your object - to provide +to_json+ or +to_xml+ serialization. + <tt>ActiveModel::Serialization</tt> provides a standard interface for your object + to provide +to_json+ serialization. class SerialPerson include ActiveModel::Serialization @@ -177,13 +177,6 @@ behavior out of the box: s = SerialPerson.new s.to_json # => "{\"name\":null}" - class SerialPerson - include ActiveModel::Serializers::Xml - end - - s = SerialPerson.new - s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support diff --git a/activemodel/bin/test b/activemodel/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/activemodel/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +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 8aa1b6f664..4e1b3f7495 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -58,7 +58,6 @@ module ActiveModel eager_autoload do autoload :JSON - autoload :Xml end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index ff7280f9e5..77e4ce3afe 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -225,7 +225,7 @@ module ActiveModel end # Declares the attributes that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix @@ -253,7 +253,7 @@ module ActiveModel end # Declares an attribute that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # # To use, pass an attribute name (as string or symbol). Be sure to declare # +define_attribute_method+ after you define any prefix, suffix or affix @@ -342,7 +342,7 @@ module ActiveModel private # The methods +method_missing+ and +respond_to?+ of this module are # invoked often in a typical rails, both of which invoke the method - # +match_attribute_method?+. The latter method iterates through an + # +matched_attribute_method+. The latter method iterates through an # array doing regular expression matches, which results in a lot of # object creations. Most of the time it returns a +nil+ match. As the # match result is always the same given a +method_name+, this cache is @@ -372,7 +372,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" @@ -429,7 +429,7 @@ module ActiveModel if respond_to_without_attributes?(method, true) super else - match = match_attribute_method?(method.to_s) + match = matched_attribute_method(method.to_s) match ? attribute_missing(match, *args, &block) : super end end @@ -454,7 +454,7 @@ module ActiveModel # but found among all methods. Which means that the given method is private. false else - !match_attribute_method?(method.to_s).nil? + !matched_attribute_method(method.to_s).nil? end end @@ -466,7 +466,7 @@ module ActiveModel private # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. - def match_attribute_method?(method_name) + def matched_attribute_method(method_name) matches = self.class.send(:attribute_method_matchers_matching, method_name) matches.detect { |match| attribute_method?(match.attr_name) } end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 287a2559d2..29e0c977ce 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -43,11 +43,11 @@ module ActiveModel # end # end # - # The last three methods are required in your object for Errors to be + # The last three methods are required in your object for +Errors+ to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translation + # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt> # you will not need to implement the last two. Likewise, using - # ActiveModel::Validations will handle the validation related methods + # <tt>ActiveModel::Validations</tt> will handle the validation related methods # for you. # # The above allows you to do: diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 1f1749af4e..213f2d5b6a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -192,7 +192,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr('/', '_') + ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index c06a64c75f..58d68d9620 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -40,7 +40,6 @@ module ActiveModel # # class Person # include ActiveModel::Serializers::JSON - # include ActiveModel::Serializers::Xml # # attr_accessor :name # @@ -55,13 +54,11 @@ module ActiveModel # person.serializable_hash # => {"name"=>nil} # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # person.as_json # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and # <tt>:include</tt>. The following are all valid examples: @@ -94,6 +91,37 @@ module ActiveModel # person.serializable_hash(except: :name) # => {"age"=>22} # person.serializable_hash(methods: :capitalized_name) # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} + # + # Example with <tt>:include</tt> option + # + # class User + # include ActiveModel::Serializers::JSON + # attr_accessor :name, :notes # Emulate has_many :notes + # def attributes + # {'name' => nil} + # end + # end + # + # class Note + # include ActiveModel::Serializers::JSON + # attr_accessor :title, :text + # def attributes + # {'title' => nil, 'text' => nil} + # end + # end + # + # note = Note.new + # note.title = 'Battle of Austerlitz' + # note.text = 'Some text here' + # + # user = User.new + # user.name = 'Napoleon' + # user.notes = [note] + # + # user.serializable_hash + # # => {"name" => "Napoleon"} + # user.serializable_hash(include: { notes: { only: 'title' }}) + # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]} def serializable_hash(options = nil) options ||= {} diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb deleted file mode 100644 index e33c766627..0000000000 --- a/activemodel/lib/active_model/serializers/xml.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/time/acts_like' - -module ActiveModel - module Serializers - # == \Active \Model XML Serializer - module Xml - extend ActiveSupport::Concern - include ActiveModel::Serialization - - included do - extend ActiveModel::Naming - end - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - - if value.acts_like?(:time) && value.respond_to?(:in_time_zone) - value = value.in_time_zone - end - - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << { xmlns: options[:namespace] } if options[:namespace] - args << { type: options[:type] } if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - [:skip_types, :dasherize, :camelize].each do |key| - merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? - end - - if records.respond_to?(:to_ary) - records = records.to_ary - - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : { type: "array" } - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = { type: record_class } - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - - unless records.class.to_s.underscore == association.to_s - merged_options[:type] = records.class.name - end - - records.to_xml merged_options - end - end - - def add_procs - if procs = options.delete(:procs) - Array(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the - # model's attributes. - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the - # attributes included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - # Sets the model +attributes+ from an XML string. Returns +self+. - # - # class Person - # include ActiveModel::Serializers::Xml - # - # attr_accessor :name, :age, :awesome - # - # def attributes=(hash) - # hash.each do |key, value| - # instance_variable_set("@#{key}", value) - # end - # end - # - # def attributes - # instance_values - # end - # end - # - # xml = { name: 'bob', age: 22, awesome:true }.to_xml - # person = Person.new - # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index c1019169e1..5f1dde4aa3 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -87,8 +87,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end - # :nodoc: - VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc: # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and @@ -130,6 +129,9 @@ module ActiveModel # end # end # + # Note that the return value of validation methods is not relevant. + # It's not possible to halt the validate callback chain. + # # 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 @@ -402,7 +404,7 @@ module ActiveModel protected def run_validations! #:nodoc: - run_callbacks :validate + _run_validate_callbacks errors.empty? end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index ee160fb483..1bcfedb35d 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -42,9 +42,10 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:accept</tt> - Specifies value that is considered accepted. - # The default value is a string "1", which makes it easy to relate to - # an HTML checkbox. This should be set to +true+ if you are validating + # * <tt>:accept</tt> - Specifies a value that is considered accepted. + # Also accepts an array of possible values. The default value is + # an array ["1", true], which makes it easy to relate to an HTML + # checkbox. This should be set to, or include, +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index b4301c23e4..4b58ef66e3 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -109,7 +109,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - run_callbacks(:validation) { super } + _run_validation_callbacks { super } end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f342d27275..6f4276cc2a 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,7 +29,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't # be part of. This can be supplied as a proc, lambda or symbol which returns an - # enumerable. If the enumerable is a range the test is performed with + # enumerable. If the enumerable is a numerical, time or datetime range the test + # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When + # using a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb new file mode 100644 index 0000000000..2176115334 --- /dev/null +++ b/activemodel/lib/active_model/validations/helper_methods.rb @@ -0,0 +1,13 @@ +module ActiveModel + module Validations + module HelperMethods # :nodoc: + private + def _merge_attributes(attr_names) + options = attr_names.extract_options!.symbolize_keys + attr_names.flatten! + options[:attributes] = attr_names + options + end + end + end +end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index c84025f083..03e0ef56d8 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,9 +28,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc, lambda or symbol which returns an enumerable. If the - # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, - # otherwise with <tt>include?</tt>. When using a proc or lambda the instance - # under validation is passed as an argument. + # enumerable is a numerical, time or datetime range the test is performed + # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using + # a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is # not included in the list"). diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c22a58f9e1..910cca2f49 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,8 +1,6 @@ require "active_support/core_ext/string/strip" module ActiveModel - - # == Active \Model Length Validator module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze @@ -100,8 +98,9 @@ module ActiveModel module HelperMethods - # Validates that the specified attribute matches the length restrictions - # supplied. Only one option can be used at a time: + # Validates that the specified attributes match the length restrictions + # supplied. Only one constraint option can be used at a time apart from + # +:minimum+ and +:maximum+ that can be combined together: # # class Person < ActiveRecord::Base # validates_length_of :first_name, maximum: 30 @@ -120,14 +119,18 @@ module ActiveModel # end # end # - # Configuration options: + # Constraint options: + # # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by - # default if not used with :minimum. + # default if not used with +:minimum+. # * <tt>:is</tt> - The exact size of the attribute. # * <tt>:within</tt> - A range specifying the minimum and maximum size of # the attribute. # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>. + # + # Other options: + # # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. # * <tt>:too_long</tt> - The error message if the attribute goes over the diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bda436d8d0..1da4df21e7 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -115,7 +115,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?('::') ? key.constantize : const_get(key) + validator = key.include?('::'.freeze) ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index a2531327bf..6de01b3392 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,15 +1,5 @@ module ActiveModel module Validations - module HelperMethods - private - def _merge_attributes(attr_names) - options = attr_names.extract_options!.symbolize_keys - attr_names.flatten! - options[:attributes] = attr_names - options - end - end - class WithValidator < EachValidator # :nodoc: def validate_each(record, attr, val) method_name = options[:with] diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 3fa63917d0..f6d171bec6 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -27,14 +27,6 @@ class ErrorsTest < ActiveModel::TestCase end end - def setup - @mock_generator = MiniTest::Mock.new - end - - def teardown - @mock_generator.verify - end - def test_delete errors = ActiveModel::Errors.new(self) errors[:foo] << 'omg' @@ -307,8 +299,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do assert_deprecated do person.errors.add_on_empty :name end @@ -317,9 +308,8 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message for multiple attributes" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - @mock_generator.expect(:call, nil, [:age, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + 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 @@ -328,8 +318,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message with custom default message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, { message: 'custom' }]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, { message: 'custom' }]) do assert_deprecated do person.errors.add_on_empty :name, message: 'custom' end @@ -339,8 +328,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message with empty string value" do person = Person.new person.name = '' - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do assert_deprecated do person.errors.add_on_empty :name end @@ -349,8 +337,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do assert_deprecated do person.errors.add_on_blank :name end @@ -359,9 +346,8 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message for multiple attributes" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, {}]) - @mock_generator.expect(:call, nil, [:age, :blank, {}]) - person.errors.stub(:generate_message, @mock_generator) do + 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 @@ -370,8 +356,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message with custom default message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, { message: 'custom' }]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :blank, { message: 'custom' }]) do assert_deprecated do person.errors.add_on_blank :name, message: 'custom' end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 0d179ea9ad..c100646837 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -11,6 +11,7 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'minitest/mock' @@ -22,3 +23,7 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveModel::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb deleted file mode 100644 index 22fca5bd17..0000000000 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'cases/helper' -require 'models/contact' -require 'active_support/core_ext/object/instance_variables' -require 'ostruct' - -module Admin - class Contact < ::Contact - end -end - -class Customer < Struct.new(:name) -end - -class Address - include ActiveModel::Serializers::Xml - - attr_accessor :street, :city, :state, :zip, :apt_number - - def attributes - instance_values - end -end - -class SerializableContact < Contact - def serializable_hash(options={}) - super(options.merge(only: [:name, :age])) - end -end - -class XmlSerializationTest < ActiveModel::TestCase - def setup - @contact = Contact.new - @contact.name = 'aaron stack' - @contact.age = 25 - @contact.created_at = Time.utc(2006, 8, 1) - @contact.awesome = false - customer = Customer.new - customer.name = "John" - @contact.preferences = customer - @contact.address = Address.new - @contact.address.city = "Springfield" - @contact.address.apt_number = 35 - @contact.friends = [Contact.new, Contact.new] - @contact.contact = SerializableContact.new - end - - test "should serialize default root" do - xml = @contact.to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize namespaced root" do - xml = Admin::Contact.new(@contact.attributes).to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize default root with namespace" do - xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize custom root" do - xml = @contact.to_xml root: 'xml_contact' - assert_match %r{^<xml-contact>}, xml - assert_match %r{</xml-contact>$}, xml - end - - test "should allow undasherized tags" do - xml = @contact.to_xml root: 'xml_contact', dasherize: false - assert_match %r{^<xml_contact>}, xml - assert_match %r{</xml_contact>$}, xml - assert_match %r{<created_at}, xml - end - - test "should allow camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: true - assert_match %r{^<XmlContact>}, xml - assert_match %r{</XmlContact>$}, xml - assert_match %r{<CreatedAt}, xml - end - - test "should allow lower-camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: :lower - assert_match %r{^<xmlContact>}, xml - assert_match %r{</xmlContact>$}, xml - assert_match %r{<createdAt}, xml - end - - test "should use serializable hash" do - @contact = SerializableContact.new - @contact.name = 'aaron stack' - @contact.age = 25 - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<age type="integer">25</age>}, xml - assert_no_match %r{<awesome>}, xml - end - - test "should allow skipped types" do - xml = @contact.to_xml skip_types: true - assert_match %r{<age>25</age>}, xml - end - - test "should include yielded additions" do - xml_output = @contact.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, xml_output - end - - test "should serialize string" do - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - test "should serialize nil" do - assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms) - end - - test "should serialize integer" do - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - test "should serialize datetime" do - assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - test "should serialize boolean" do - assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml - end - - test "should serialize array" do - assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social) - end - - test "should serialize hash" do - assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network) - end - - test "should serialize yaml" do - assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml - end - - test "should call proc on object" do - proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<nationality>unknown</nationality>}, xml - end - - test "should supply serializable to second proc argument" do - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml - end - - test "should serialize string correctly when type passed" do - xml = @contact.to_xml type: 'Contact' - assert_match %r{<contact type="Contact">}, xml - assert_match %r{<name>aaron stack</name>}, xml - end - - test "include option with singular association" do - xml = @contact.to_xml include: :address, indent: 0 - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - end - - test "include option with plural association" do - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - class FriendList - def initialize(friends) - @friends = friends - end - - def to_ary - @friends - end - end - - test "include option with ary" do - @contact.friends = FriendList.new(@contact.friends) - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "multiple includes" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ] - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "include with options" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } } - assert xml.include?(%(><address><city>Springfield</city></address>)) - end - - test "propagates skip_types option to included associations" do - xml = @contact.to_xml include: :friends, indent: 0, skip_types: true - assert_match %r{<friends>}, xml - assert_match %r{<friend>}, xml - end - - test "propagates skip-types option to included associations and attributes" do - xml = @contact.to_xml skip_types: true, include: :address, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number>}, xml - end - - test "propagates camelize option to included associations and attributes" do - xml = @contact.to_xml camelize: true, include: :address, indent: 0 - assert_match %r{<Address>}, xml - assert_match %r{<AptNumber type="integer">}, xml - end - - test "propagates dasherize option to included associations and attributes" do - xml = @contact.to_xml dasherize: false, include: :address, indent: 0 - assert_match %r{<apt_number type="integer">}, xml - end - - test "don't propagate skip_types if skip_types is defined at the included association level" do - xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate camelize if camelize is defined at the included association level" do - xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate dasherize if dasherize is defined at the included association level" do - xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "association with sti" do - xml = @contact.to_xml(include: :contact) - assert xml.include?(%(<contact type="SerializableContact">)) - end -end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index 9c2114d83d..d3995ad5af 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -50,6 +50,20 @@ class AcceptanceValidationTest < ActiveModel::TestCase assert t.valid? end + def test_terms_of_service_agreement_with_multiple_accept_values + Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) + + t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") + assert t.invalid? + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = 1 + assert t.valid? + + t.terms_of_service = "I concur." + assert t.valid? + end + def test_validates_acceptance_of_for_ruby_class Person.validates_acceptance_of :karma diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index b269c3691a..005bc15df5 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_support/core_ext/numeric/time' require 'models/topic' require 'models/person' @@ -64,6 +65,22 @@ class ExclusionValidationTest < ActiveModel::TestCase assert t.valid? end + def test_validates_exclusion_of_with_range + Topic.validates_exclusion_of :content, in: ("a".."g") + + assert Topic.new(content: 'g').invalid? + assert Topic.new(content: 'h').valid? + end + + def test_validates_exclusion_of_with_time_range + Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago + + assert Topic.new(created_at: 5.days.ago).invalid? + assert Topic.new(created_at: 3.days.ago).invalid? + assert Topic.new(created_at: 7.days.ago).valid? + assert Topic.new(created_at: 1.day.ago).valid? + end + def test_validates_inclusion_of_with_symbol Person.validates_exclusion_of :karma, in: :reserved_karmas diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index be17575402..09d7226b5a 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -11,7 +11,6 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new I18n.backend.store_translations('en', errors: { messages: { custom: nil } }) - @mock_generator = MiniTest::Mock.new end def teardown @@ -19,7 +18,6 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.replace @old_load_path I18n.backend = @old_backend I18n.backend.reload! - @mock_generator.verify end def test_full_message_encoding @@ -32,8 +30,7 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, 'not found') - @mock_generator.expect(:call, "Person's name", [:name, default: 'Name']) - Person.stub(:human_attribute_name, @mock_generator) do + assert_called_with(Person, :human_attribute_name, [:name, default: 'Name'], returns: "Person's name") do assert_equal ["Person's name not found"], @person.errors.full_messages end end @@ -58,206 +55,175 @@ class I18nValidationTest < ActiveModel::TestCase [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }] ] - # validates_confirmation_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @mock_generator.expect(:call, nil, [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_acceptance_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_acceptance_of on generated message #{name}" do Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false) - @mock_generator.expect(:call, nil, [:title, :accepted, generate_message_options]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :accepted, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_presence_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_presence_of on generated message #{name}" do Person.validates_presence_of :title, validation_options - @mock_generator.expect(:call, nil, [:title, :blank, generate_message_options]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :blank, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_length_of :within too short w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| - test "validates_length_of for :withing on generated message when too short #{name}" do + test "validates_length_of for :within on generated message when too short #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) - @mock_generator.expect(:call, nil, [:title, :too_short, generate_message_options.merge(count: 3)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :too_short, generate_message_options.merge(count: 3)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_length_of :within too long w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :too_long generated message #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) @person.title = 'this title is too long' - @mock_generator.expect(:call, nil, [:title, :too_long, generate_message_options.merge(count: 5)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :too_long, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_length_of :is w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :is on generated message #{name}" do Person.validates_length_of :title, validation_options.merge(is: 5) - @mock_generator.expect(:call, nil, [:title, :wrong_length, generate_message_options.merge(count: 5)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :wrong_length, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_format_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/) @person.title = '72x' - @mock_generator.expect(:call, nil, [:title, :invalid, generate_message_options.merge(value: '72x')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :invalid, generate_message_options.merge(value: '72x')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_inclusion_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'z' - @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_inclusion_of using :within w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of using :within on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'z' - @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_exclusion_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_exclusion_of using :within w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of using :within generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_numericality_of without :only_integer w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of generated message #{name}" do Person.validates_numericality_of :title, validation_options @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :not_a_number, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :not_a_number, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_numericality_of with :only_integer w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :only_integer on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true) @person.title = '0.0' - @mock_generator.expect(:call, nil, [:title, :not_an_integer, generate_message_options.merge(value: '0.0')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :not_an_integer, generate_message_options.merge(value: '0.0')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_numericality_of :odd w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :odd on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true) @person.title = 0 - @mock_generator.expect(:call, nil, [:title, :odd, generate_message_options.merge(value: 0)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :odd, generate_message_options.merge(value: 0)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - # validates_numericality_of :less_than w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :less_than on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0) @person.title = 1 - @mock_generator.expect(:call, nil, [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end end - - # To make things DRY this macro is defined to define 3 tests for every validation case. + # To make things DRY this macro is created to define 3 tests for every validation case. def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) if error_type == :confirmation attribute = :title_confirmation else attribute = :title end - # test "validates_confirmation_of finds custom model key translation when blank" + test "#{validation} finds custom model key translation when #{error_type}" do I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message' } } } } } } I18n.backend.store_translations 'en', errors: { messages: { error_type => 'global message'}} @@ -267,7 +233,6 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ['custom message'], @person.errors[attribute] end - # test "validates_confirmation_of finds custom model key translation with interpolation when blank" test "#{validation} finds custom model key translation with interpolation when #{error_type}" do I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message with %{extra}' } } } } } } I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} } @@ -277,7 +242,6 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ['custom message with extra information'], @person.errors[attribute] end - # test "validates_confirmation_of finds global default key translation when blank" test "#{validation} finds global default key translation when #{error_type}" do I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} } @@ -287,27 +251,19 @@ class I18nValidationTest < ActiveModel::TestCase end end - # validates_confirmation_of w/o mocha - set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge| Person.validates_confirmation_of :title, options_to_merge person.title_confirmation = 'foo' end - # validates_acceptance_of w/o mocha - set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge| Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false) end - # validates_presence_of w/o mocha - set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge| Person.validates_presence_of :title, options_to_merge end - # validates_length_of :within w/o mocha - set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge| Person.validates_length_of :title, options_to_merge.merge(within: 3..5) end @@ -317,61 +273,43 @@ class I18nValidationTest < ActiveModel::TestCase person.title = "too long" end - # validates_length_of :is w/o mocha - set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge| Person.validates_length_of :title, options_to_merge.merge(is: 5) end - # validates_format_of w/o mocha - set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| Person.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/) end - # validates_inclusion_of w/o mocha - set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge| Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c)) end - # validates_exclusion_of w/o mocha - set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge| Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c)) person.title = 'a' end - # validates_numericality_of without :only_integer w/o mocha - set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge person.title = 'a' end - # validates_numericality_of with :only_integer w/o mocha - set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true) person.title = '1.0' end - # validates_numericality_of :odd w/o mocha - set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true) person.title = 0 end - # validates_numericality_of :less_than w/o mocha - set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0) person.title = 1 end - # test with validates_with - def test_validations_with_message_symbol_must_translate I18n.backend.store_translations 'en', errors: { messages: { custom_error: "I am a custom error" } } Person.validates_presence_of :title, message: :custom_error diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 9ee8b79da9..03c7943308 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -97,7 +97,7 @@ class ValidatesWithTest < ActiveModel::TestCase test "passes all configuration options to the validator class" do topic = Topic.new - validator = MiniTest::Mock.new + validator = Minitest::Mock.new validator.expect(:new, validator, [{foo: :bar, if: "1 == 1", class: Topic}]) validator.expect(:validate, nil, [topic]) validator.expect(:is_a?, false, [Symbol]) diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index bcfd267a34..113ab0bc1f 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -4,7 +4,6 @@ class Contact include ActiveModel::Validations include ActiveModel::Serializers::JSON - include ActiveModel::Serializers::Xml attr_accessor :id, :name, :age, :created_at, :awesome, :preferences attr_accessor :address, :friends, :contact diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index edbfe38753..bd86dfb88e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,284 @@ +* Descriptive error message when fixtures contain a missing column. + + Closes #21201. + + *Yves Senn* + +* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to + postgresql commands (like `pg_dump`) is not successful. + + *Bryan Paxton*, *Nate Berkopec* + +* Add `ActiveRecord::Relation#in_batches` to work with records and relations + in batches. + + Available options are `of` (batch size), `load`, `begin_at`, and `end_at`. + + Examples: + + Person.in_batches.each_record(&:party_all_night!) + Person.in_batches.update_all(awesome: true) + Person.in_batches.delete_all + Person.in_batches.each do |relation| + relation.delete_all + sleep 10 # Throttles the delete queries + end + + Closes #20933. + + *Sina Siadat* + +* Added methods for PostgreSQL geometric data types to use in migrations + + Example: + + create_table :foo do |t| + t.line :foo_line + t.lseg :foo_lseg + t.box :foo_box + t.path :foo_path + t.polygon :foo_polygon + t.circle :foo_circle + end + + *Mehmet Emin İNAÇ* + +* Add `cache_key` to ActiveRecord::Relation. + + Example: + + @users = User.where("name like ?", "%Alberto%") + @users.cache_key + => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000" + + *Alberto Fernández-Capel* + +* Properly allow uniqueness validations on primary keys. + + Fixes #20966. + + *Sean Griffin*, *presskey* + +* Don't raise an error if an association failed to destroy when `destroy` was + called on the parent (as opposed to `destroy!`). + + Fixes #20991. + + *Sean Griffin* + +* `ActiveRecord::RecordNotFound` modified to store model name, primary_key and + id of the caller model. It allows the catcher of this exception to make + a better decision to what to do with it. + + Example: + + class SomeAbstractController < ActionController::Base + rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404 + + private def redirect_to_404(e) + return redirect_to(posts_url) if e.model == 'Post' + raise + end + end + + *Sameer Rahmani* + +* Deprecate the keys for association `restrict_dependent_destroy` errors in favor + of new key names. + + Previously `has_one` and `has_many` associations were using the + `one` and `many` keys respectively. Both of these keys have special + meaning in I18n (they are considered to be pluralizations) so by + renaming them to `has_one` and `has_many` we make the messages more explicit + and most importantly they don't clash with linguistical systems that need to + validate translation keys (and their pluralizations). + + The `:'restrict_dependent_destroy.one'` key should be replaced with + `:'restrict_dependent_destroy.has_one'`, and `:'restrict_dependent_destroy.many'` + with `:'restrict_dependent_destroy.has_many'`. + + *Roque Pinel*, *Christopher Dell* + +* Fix state being carried over from previous transaction. + + Considering the following example where `name` is a required attribute. + Before we had `new_record?` returning `true` for a persisted record: + + author = Author.create! name: 'foo' + author.name = nil + author.save # => false + author.new_record? # => true + + Fixes #20824. + + *Roque Pinel* + +* Correctly ignore `mark_for_destruction` when `autosave` isn't set to `true` + when validating associations. + + Fixes #20882. + + *Sean Griffin* + +* Fix a bug where counter_cache doesn't always work with polymorphic + relations. + + Fixes #16407. + + *Stefan Kanev*, *Sean Griffin* + +* Ensure that cyclic associations with autosave don't cause duplicate errors + to be added to the parent record. + + Fixes #20874. + + *Sean Griffin* + +* Ensure that `ActionController::Parameters` can still be passed to nested + attributes. + + Fixes #20922. + + *Sean Griffin* + +* Deprecate force association reload by passing a truthy argument to + association method. + + For collection association, you can call `#reload` on association proxy to + force a reload: + + @user.posts.reload # Instead of @user.posts(true) + + For singular association, you can call `#reload` on the parent object to + clear its association cache then call the association method: + + @user.reload.profile # Instead of @user.profile(true) + + Passing a truthy argument to force association to reload will be removed in + Rails 5.1. + + *Prem Sichanugrist* + +* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch` + from the concurrent-ruby gem. + + *Jerry D'Antonio* + +* Fix through associations using scopes having the scope merged multiple + times. + + Fixes #20721. + Fixes #20727. + + *Sean Griffin* + +* `ActiveRecord::Base.dump_schema_after_migration` applies migration tasks + other than `db:migrate`. (eg. `db:rollback`, `db:migrate:dup`, ...) + + Fixes #20743. + + *Yves Senn* + +* Add alternate syntax to make `change_column_default` reversible. + + User can pass in `:from` and `:to` to make `change_column_default` command + become reversible. + + Example: + + change_column_default :posts, :status, from: nil, to: "draft" + change_column_default :users, :authorized, from: true, to: false + + *Prem Sichanugrist* + +* Prevent error when using `force_reload: true` on an unassigned polymorphic + belongs_to association. + + Fixes #20426. + + *James Dabbs* + +* Correctly raise `ActiveRecord::AssociationTypeMismatch` when assigning + a wrong type to a namespaced association. + + Fixes #20545. + + *Diego Carrion* + +* `validates_absence_of` respects `marked_for_destruction?`. + + Fixes #20449. + + *Yves Senn* + +* Include the `Enumerable` module in `ActiveRecord::Relation` + + *Sean Griffin & bogdan* + +* Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given. + + *Sean Griffin* + +* Let `WITH` queries (Common Table Expressions) be explainable. + + *Vladimir Kochnev* + +* Make `remove_index :table, :column` reversible. + + *Yves Senn* + +* Fixed an error which would occur in dirty checking when calling + `update_attributes` from a getter. + + Fixes #20531. + + *Sean Griffin* + +* Make `remove_foreign_key` reversible. Any foreign key options must be + specified, similar to `remove_column`. + + *Aster Ryan* + +* Add `:_prefix` and `:_suffix` options to `enum` definition. + + Fixes #17511, #17415. + + *Igor Kapkov* + +* Correctly handle decimal arrays with defaults in the schema dumper. + + Fixes #20515. + + *Sean Griffin & jmondo* + +* Deprecate the PostgreSQL `:point` type in favor of a new one which will return + `Point` objects instead of an `Array` + + *Sean Griffin* + +* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated + as columns. + + Fixes #20360. + + *Sean Griffin* + +* Do not set `sql_mode` if `strict: :default` is specified. + + # config/database.yml + production: + adapter: mysql2 + database: foo_prod + user: foo + strict: :default + + *Ryuta Kamizono* + +* Allow proc defaults to be passed to the attributes API. See documentation + for examples. + + *Sean Griffin*, *Kir Shatrov* + * SQLite: `:collation` support for string and text columns. Example: @@ -30,7 +311,8 @@ *Jonathan Worek* -* Pass `:extend` option for `has_and_belongs_to_many` associations to the underlying `has_many :through`. +* Pass `:extend` option for `has_and_belongs_to_many` associations to the + underlying `has_many :through`. *Jaehyun Shin* @@ -74,6 +356,10 @@ *Ryuta Kamizono* +* Remove `ActiveRecord::Serialization::XmlSerializer` from core. + + *Zachary Scott* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* @@ -265,7 +551,7 @@ *Josef Šimánek* -* Fixed ActiveRecord::Relation#becomes! and changed_attributes issues for type +* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type columns. Fixes #17139. @@ -448,8 +734,8 @@ *Henrik Nygren* -* Fixed ActiveRecord::Relation#group method when an argument is an SQL - reserved key word: +* Fixed `ActiveRecord::Relation#group` method when an argument is an SQL + reserved keyword: Example: @@ -458,7 +744,7 @@ *Bogdan Gusiev* -* Added the `#or` method on ActiveRecord::Relation, allowing use of the OR +* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR operator to combine WHERE or HAVING clauses. Example: @@ -658,7 +944,7 @@ The preferred method to halt a callback chain from now on is to explicitly `throw(:abort)`. - In the past, returning `false` in an ActiveRecord `before_` callback had the + In the past, returning `false` in an Active Record `before_` callback had the side effect of halting the callback chain. This is not recommended anymore and, depending on the value of the `config.active_support.halt_callback_chains_on_return_false` option, will diff --git a/activerecord/Rakefile b/activerecord/Rakefile index a619204e6f..8ea22fd901 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -83,15 +83,6 @@ end task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] end -rule '.sqlite3' do |t| - sh %Q{sqlite3 "#{t.name}" "create table a (a integer); drop table a;"} -end - -task :test_sqlite3 => [ - 'test/fixtures/fixture_database.sqlite3', - 'test/fixtures/fixture_database_2.sqlite3' -] - namespace :db do namespace :mysql do desc 'Build the MySQL test databases' diff --git a/activerecord/bin/test b/activerecord/bin/test new file mode 100755 index 0000000000..f8adf2aabc --- /dev/null +++ b/activerecord/bin/test @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +module Minitest + def self.plugin_active_record_options(opts, options) + opts.separator "" + opts.separator "Active Record options:" + opts.on("-a", "--adapter [ADAPTER]", + "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql, mysql2, postgresql)") do |adapter| + ENV["ARCONN"] = adapter.strip + end + + opts + end +end + +Minitest.extensions.unshift 'active_record' + +exit Minitest.run(ARGV) diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 1844b29ccb..264f869c68 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -53,7 +53,9 @@ module ActiveRecord autoload :Persistence autoload :QueryCache autoload :Querying + autoload :CollectionCacheKey autoload :ReadonlyAttributes + autoload :RecordInvalid, 'active_record/validations' autoload :Reflection autoload :RuntimeRegistry autoload :Sanitization diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1ca648d48d..a830b0e0e4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -61,12 +61,18 @@ module ActiveRecord end end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") end end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + + class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") @@ -79,12 +85,18 @@ module ActiveRecord end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: + class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") end end + class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") @@ -955,20 +967,16 @@ module ActiveRecord # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record doesn't know anything about these inverse relationships and so no object - # loading optimization is possible. For example: + # Active Record can guess the inverse of the association based on the name + # of the class. The result is the following: # # d = Dungeon.first # t = d.traps.first - # d.level == t.dungeon.level # => true - # d.level = 10 - # d.level == t.dungeon.level # => false + # d.object_id == t.dungeon.object_id # => true # # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same object data from the database, but are actually different in-memory copies - # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For - # example, if we changed our model definitions to: + # the same in-memory instance since the association matches the name of the class. + # The result would be the same if we added +:inverse_of+ to our model definitions: # # class Dungeon < ActiveRecord::Base # has_many :traps, inverse_of: :dungeon @@ -983,15 +991,14 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same - # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+. - # # There are limitations to <tt>:inverse_of</tt> support: # # * does not work with <tt>:through</tt> associations. # * does not work with <tt>:polymorphic</tt> associations. # * for +belongs_to+ associations +has_many+ inverse associations are ignored. # + # For more information, see the documentation for the +:inverse_of+ option. + # # == Deleting from associations # # === Dependent associations diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 930f678ae8..c7b396f3d4 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -211,9 +211,12 @@ module ActiveRecord # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch!(record) - unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" - raise ActiveRecord::AssociationTypeMismatch, message + unless record.is_a?(reflection.klass) + fresh_class = reflection.class_name.safe_constantize + unless fresh_class && record.is_a?(fresh_class) + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + raise ActiveRecord::AssociationTypeMismatch, message + end end end @@ -248,6 +251,14 @@ module ActiveRecord initialize_attributes(record) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 2416167834..a140dc239c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -149,6 +149,7 @@ module ActiveRecord scope.where_clause += item.where_clause scope.order_values |= item.order_values + scope.unscope!(*item.unscope_values) end reflection = reflection.next diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 265a65c4c1..260a0c6a2d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -107,7 +107,7 @@ module ActiveRecord end def stale_state - result = owner._read_attribute(reflection.foreign_key) + result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) } result && result.to_s end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97eb007f62..6e4a53f7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,16 +33,24 @@ module ActiveRecord::Associations::Builder if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? - model = reflection.klass + elsif attribute_changed?(foreign_key) && !new_record? + if reflection.polymorphic? + model = attribute(reflection.foreign_type).try(:constantize) + model_was = attribute_was(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 if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) end - if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(cache_column, foreign_key_was) + + if foreign_key_was && model_was.respond_to?(:decrement_counter) + model_was.decrement_counter(cache_column, foreign_key_was) end end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index ffd9c9d6fc..b18d99d54e 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 @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) @@ -72,11 +72,15 @@ module ActiveRecord::Associations::Builder self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 6caadb4ce8..256df3ca11 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,3 +1,5 @@ +require "active_support/deprecation" + module ActiveRecord module Associations # = Active Record Association Collection @@ -28,6 +30,12 @@ module ActiveRecord # 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? reload @@ -54,8 +62,10 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end @@ -432,12 +442,7 @@ module ActiveRecord private def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end + return scope.to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 685c3a5f17..b5a8c81fe4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -470,15 +470,16 @@ module ActiveRecord @association.destroy_all end - # Deletes the +records+ supplied and removes them from the collection. For - # +has_many+ associations, the deletion is done according to the strategy - # specified by the <tt>:dependent</tt> option. Returns an array with the + # Deletes the +records+ supplied from the collection according to the strategy + # specified by the +:dependent+ option. If no +:dependent+ option is given, + # then it will follow the default strategy. Returns an array with the # deleted records. # - # If no <tt>:dependent</tt> option is given, then it will follow the default - # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign - # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default - # strategy is +delete_all+. + # For +has_many :through+ associations, the default deletion strategy is + # +:delete_all+. + # + # For +has_many+ associations, the default deletion strategy is +:nullify+. + # This sets the foreign keys to +NULL+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default @@ -780,7 +781,7 @@ module ActiveRecord # person.pets.any? # => false # # person.pets << Pet.new(name: 'Snoop') - # person.pets.count # => 0 + # person.pets.count # => 1 # person.pets.any? # => true # # You can also pass a +block+ to define criteria. The behavior @@ -970,7 +971,7 @@ module ActiveRecord alias_method :append, :<< def prepend(*args) - raise NoMethodError, "prepend on association is not defined. Please use << or append" + raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index ca27c9fdde..19cdd7f470 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,7 +16,14 @@ module ActiveRecord when :restrict_with_error unless empty? record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + 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) throw(:abort) end @@ -80,8 +87,15 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) + if reflection.options[:counter_cache] || (inverse = inverse_which_updates_counter_cache(reflection)) && inverse.options[:counter_cache] + owner.attribute_present?(cached_counter_attribute_name(reflection)) + end end def cached_counter_attribute_name(reflection = reflection()) 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 cd79266952..1aa6a2ca74 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -133,7 +133,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each { |record| record.run_callbacks :destroy } + scope.each(&:_run_destroy_callbacks) arel = scope.arel diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 41a75b820e..5a92bc5e8a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,7 +12,14 @@ module ActiveRecord when :restrict_with_error if load_target record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + 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) throw(:abort) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 97f4bd3811..3992a240b9 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -10,13 +10,13 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # # columns: title, sales + # # columns: title, sales, author_id # end # # When you load an author with all associated books Active Record will make # multiple queries like this: # - # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a # # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) @@ -116,7 +116,7 @@ module ActiveRecord when String preloaders_for_one(association.to_sym, records, scope) else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + raise ArgumentError, "#{association.inspect} was not recognized for preload" end end @@ -160,7 +160,7 @@ module ActiveRecord h end - class AlreadyLoaded + class AlreadyLoaded # :nodoc: attr_reader :owners, :reflection def initialize(klass, owners, reflection, preload_scope) @@ -175,7 +175,7 @@ module ActiveRecord end end - class NullPreloader + class NullPreloader # :nodoc: def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end def self.preloaded_records; []; end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 58d0f7d65d..03cb8cb8c3 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,9 +1,17 @@ +require "active_support/deprecation" + 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 + 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? reload @@ -39,12 +47,7 @@ module ActiveRecord end def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + return scope.limit(1).to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index af1bce523c..d0ec3e8015 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -15,12 +15,6 @@ module ActiveRecord scope = super reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all - - reflection_scope = reflection.scope - if reflection_scope && reflection_scope.arity.zero? - relation = relation.merge(reflection_scope) - end - scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) @@ -82,13 +76,21 @@ module ActiveRecord def ensure_mutable unless source_reflection.belongs_to? - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + else + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end end end def ensure_not_nested if reflection.nested? - raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection) + else + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9d58a19304..0862306749 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -42,7 +42,7 @@ module ActiveRecord def [](name) @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ @@ -230,7 +230,15 @@ module ActiveRecord # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) return false unless super - name = name.to_s + + case name + when :to_partial_path + name = "to_partial_path".freeze + when :to_model + name = "to_model".freeze + else + name = name.to_s + end # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. @@ -377,27 +385,27 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 7ba907f786..0171ef3bdf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -108,6 +108,7 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) + clear_changed_attributes_cache if attribute_changed_by_setter?(attr) clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else @@ -176,7 +177,11 @@ module ActiveRecord @cached_changed_attributes = changed_attributes yield ensure - remove_instance_variable(:@cached_changed_attributes) + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0d989c2eca..2363cf7608 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,7 +37,7 @@ module ActiveRecord protected def define_method_attribute(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d0d8a968c5..60eecab0d0 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,6 +11,18 @@ module ActiveRecord # serialized object must be of that class on assignment and retrieval. # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of + # +Array+, will always be persisted as null. + # + # Keep in mind that database adapters handle certain serialization tasks + # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be + # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ + # objects transparently. There is no need to use +serialize+ in this + # case. + # + # For more complex cases, such as conversion to or from your application + # domain objects, consider using the ActiveRecord::Attributes API. + # # ==== Parameters # # * +attr_name+ - The field name that should be serialized. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index ab017c7b54..07d5e7d38e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -24,7 +24,7 @@ module ActiveRecord protected def define_method_attribute=(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index c89099589e..8b2c4c7170 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -82,6 +82,14 @@ module ActiveRecord # # StoreListing.new.my_string # => "new default" # + # class Product < ActiveRecord::Base + # attribute :my_default_proc, :datetime, default: -> { Time.now } + # end + # + # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600 + # sleep 1 + # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600 + # # Attributes do not need to be backed by a database column. # # class MyModel < ActiveRecord::Base @@ -204,7 +212,8 @@ module ActiveRecord # # +default+ The default value to use when no value is provided. If this option # is not passed, the previous default value (if any) will be used. - # Otherwise, the default will be +nil+. + # Otherwise, the default will be +nil+. A proc can also be passed, and + # will be called once each time a new value is needed. # # +user_provided_default+ Whether the default value should be cast using # +cast+ or +deserialize+. diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0792d19c3e..dbb0e2fab2 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -222,6 +222,7 @@ module ActiveRecord true end validate validation_method + after_validation :_ensure_no_duplicate_errors end end end @@ -324,7 +325,7 @@ module ActiveRecord # 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) - return true if record.destroyed? || record.marked_for_destruction? + return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) @@ -456,5 +457,11 @@ module ActiveRecord end end end + + def _ensure_no_duplicate_errors + errors.messages.each_key do |attribute| + errors[attribute].uniq! + end + end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c918e88590..55a7e053bc 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -280,6 +280,7 @@ module ActiveRecord #:nodoc: extend Explain extend Enum extend Delegation::DelegateCache + extend CollectionCacheKey include Core include Persistence diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 3027ce928e..ccdbebbc77 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -206,7 +206,8 @@ module ActiveRecord # == Ordering callbacks # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ - # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: destroy</tt> option. # # Let's look at the code below: # @@ -289,24 +290,28 @@ module ActiveRecord end def destroy #:nodoc: - run_callbacks(:destroy) { super } + _run_destroy_callbacks { super } + rescue RecordNotDestroyed => e + @_association_destroy_exception = e + false end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private + def create_or_update(*) #:nodoc: - run_callbacks(:save) { super } + _run_save_callbacks { super } end def _create_record #:nodoc: - run_callbacks(:create) { super } + _run_create_callbacks { super } end def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb new file mode 100644 index 0000000000..3c4ca3d116 --- /dev/null +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module CollectionCacheKey + + def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: + query_signature = Digest::MD5.hexdigest(collection.to_sql) + key = "#{collection.model_name.cache_key}/query-#{query_signature}" + + if collection.loaded? + size = collection.size + timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + else + column_type = type_for_attribute(timestamp_column.to_s) + column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + + query = collection + .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") + .unscope(:order) + result = connection.select_one(query) + + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + + if timestamp + "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + else + "#{key}-#{size}" + end + end + end +end 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 6535121075..282af220fb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -508,7 +508,7 @@ module ActiveRecord synchronize do remove_connection_from_thread_cache conn - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -764,7 +764,7 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c 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 38dd9578fe..1e13b24867 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -289,8 +289,12 @@ module ActiveRecord columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| 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 158b773e11..d17e272ed1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -592,10 +592,11 @@ module ActiveRecord # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # t.change_default(:status, from: nil, to: "draft") # # See SchemaStatements#change_column_default - def change_default(column_name, default) - @base.change_column_default(name, column_name, default) + def change_default(column_name, default_or_changes) + @base.change_column_default(name, column_name, default_or_changes) end # Removes the column(s) from the table definition. 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 ed19819d63..a30945d0ee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -460,7 +460,12 @@ module ActiveRecord # # change_column_default(:users, :email, nil) # - def change_column_default(table_name, column_name, default) + # Passing a hash containing +:from+ and +:to+ will make this change + # reversible in migration: + # + # change_column_default(:posts, :state, from: nil, to: "draft") + # + def change_column_default(table_name, column_name, default_or_changes) raise NotImplementedError, "change_column_default is not implemented" end @@ -587,7 +592,7 @@ module ActiveRecord # # Removes the +index_accounts_on_column+ in the +accounts+ table. # - # remove_index :accounts, :column + # remove_index :accounts, :branch_id # # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. # @@ -662,7 +667,7 @@ module ActiveRecord # [<tt>:foreign_key</tt>] # Add an appropriate foreign key. Defaults to false. # [<tt>:polymorphic</tt>] - # Wether an additional +_type+ column should be added. Defaults to false. + # Whether an additional +_type+ column should be added. Defaults to false. # # ====== Create a user_id integer column # @@ -756,9 +761,9 @@ module ActiveRecord # [<tt>:name</tt>] # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. # [<tt>:on_delete</tt>] - # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] - # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -777,7 +782,10 @@ module ActiveRecord execute schema_creation.accept(at) end - # Removes the given foreign key from the table. + # Removes the given foreign key from the table. Any option parameters provided + # will be used to re-add the foreign key in case of a migration rollback. + # It is recommended that you provide any options used when creating the foreign + # key so that the migration can be reverted properly. # # Removes the foreign key on +accounts.branch_id+. # @@ -791,6 +799,7 @@ module ActiveRecord # # remove_foreign_key :accounts, name: :special_fk_name # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -828,7 +837,10 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - "#{table_name.to_s.singularize}_id" + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + "#{name.singularize}_id" end def dump_schema_information #:nodoc: @@ -1064,6 +1076,14 @@ module ActiveRecord raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end + + def extract_new_default_value(default_or_changes) + if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) + default_or_changes[:to] + else + default_or_changes + 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 6d3a21a3dc..56227ddd80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -107,6 +107,18 @@ module ActiveRecord @prepared_statements = false end + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end + end + class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) casted_binds = conn.prepare_binds_for_database(bvs) 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 00e3d2965b..af156c9c78 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -307,7 +307,7 @@ module ActiveRecord # # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version[0] >= 5 + version >= '5.0.0' end def supports_indexes_in_create? @@ -319,11 +319,11 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + version >= '5.0.0' end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + version >= '5.6.4' end def native_database_types @@ -386,6 +386,14 @@ module ActiveRecord 0 end + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, '') + end + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -629,7 +637,8 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -937,7 +946,7 @@ module ActiveRecord end def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end def mariadb? @@ -945,7 +954,7 @@ module ActiveRecord end def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + mariadb? ? false : version >= '5.7.6' end def configure_connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..b7db57c9fe 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -254,7 +254,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] + @full_version ||= @connection.server_info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 92349e2f9b..68752cdd80 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -12,6 +12,7 @@ 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/point' +require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/uuid' 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 3de794f797..25961a9869 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -45,6 +45,11 @@ module ActiveRecord delimiter == other.delimiter end + def type_cast_for_schema(value) + return super unless value.is_a?(::Array) + "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" + end + private def type_cast_array(value, method) 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 afc9383f91..87391b5dc7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -9,7 +9,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, new_value) # Postgres does not preserve insignificant whitespaces when - # roundtripping jsonb columns. This causes some false positives for + # 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. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb new file mode 100644 index 0000000000..7427a25ad5 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb @@ -0,0 +1,50 @@ +module ActiveRecord + Point = Struct.new(:x, :y) + + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Rails51Point < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :point + end + + def cast(value) + case value + when ::String + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + x, y = value.split(",") + build_point(x, y) + when ::Array + build_point(*value) + else + value + end + end + + def serialize(value) + if value.is_a?(ActiveRecord::Point) + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end + 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 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 595c635fc0..d114cad16b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -410,11 +410,12 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) # :nodoc: + def change_column_default(table_name, column_name, default_or_changes) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column + default = extract_new_default_value(default_or_changes) alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" if default.nil? # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will 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 58715978f7..b2c49989a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -12,7 +12,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 93fa3984e6..2c43c46a3d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -838,6 +838,8 @@ module ActiveRecord ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 87129c42cf..358039723f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -65,18 +65,6 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool private @@ -422,7 +410,9 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) + alter_table(table_name) do |definition| definition[column_name].default = default end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8a014e682e..ffce2173ec 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -162,11 +162,13 @@ module ActiveRecord } record = statement.execute([id], self, connection).first unless record - raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", + name, primary_key, id) end record rescue RangeError - raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" + 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: @@ -199,7 +201,7 @@ module ActiveRecord end def find_by!(*args) # :nodoc: - find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name) end def initialize_generated_modules # :nodoc: @@ -303,7 +305,7 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - run_callbacks :initialize + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ should be @@ -330,8 +332,8 @@ module ActiveRecord self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + _run_find_callbacks + _run_initialize_callbacks self end @@ -367,7 +369,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) + _run_initialize_callbacks @new_record = true @destroyed = false diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 2b99899e42..91a13cb0cd 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -75,6 +75,24 @@ module ActiveRecord # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # + # You can use the +:_prefix+ or +:_suffix+ options when you need to define + # multiple enums with same values. If the passed value is +true+, the methods + # are prefixed/suffixed with the name of the enum. It is also possible to + # supply a custom value: + # + # class Conversation < ActiveRecord::Base + # enum status: [:active, :archived], _suffix: true + # enum comments_status: [:active, :inactive], _prefix: :comments + # end + # + # With the above example, the bang and predicate methods along with the + # associated scopes are now prefixed and/or suffixed accordingly: + # + # conversation.active_status! + # conversation.archived_status? # => false + # + # conversation.comments_inactive! + # conversation.comments_active? # => false module Enum def self.extended(base) # :nodoc: @@ -121,6 +139,8 @@ module ActiveRecord def enum(definitions) klass = self + enum_prefix = definitions.delete(:_prefix) + enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new @@ -138,19 +158,31 @@ 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| + if enum_prefix == true + prefix = "#{name}_" + elsif enum_prefix + prefix = "#{enum_prefix}_" + end + if enum_suffix == true + suffix = "_#{name}" + elsif enum_suffix + suffix = "_#{enum_suffix}" + end + + value_method_name = "#{prefix}#{value}#{suffix}" enum_values[value] = i # def active?() status == 0 end - klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == value.to_s } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") + define_method("#{value_method_name}?") { self[name] == value.to_s } # def active!() update! status: :active end - klass.send(:detect_enum_conflict!, name, "#{value}!") - define_method("#{value}!") { update! name => value } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") + define_method("#{value_method_name}!") { update! name => value } # scope :active, -> { where status: 0 } - klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => value } + klass.send(:detect_enum_conflict!, name, value_method_name, true) + klass.scope value_method_name, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 0f1759abaa..d589620f8a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -47,6 +47,15 @@ module ActiveRecord # Raised when Active Record cannot find record by given id or set of ids. class RecordNotFound < ActiveRecordError + attr_reader :model, :primary_key, :id + + def initialize(message = nil, model = nil, primary_key = nil, id = nil) + @primary_key = primary_key + @model = model + @id = id + + super(message) + end end # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 6a49936644..9adabd7819 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -19,7 +19,7 @@ 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) - EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i + 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 end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 1ec8f818cd..f1dc56df63 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -110,7 +110,7 @@ module ActiveRecord # <% 1.upto(1000) do |i| %> # fix_<%= i %>: # id: <%= i %> - # name: guy_<%= 1 %> + # name: guy_<%= i %> # <% end %> # # This will create 1000 very simple fixtures. @@ -615,7 +615,6 @@ module ActiveRecord # a list of rows to insert to that table. def table_rows now = config.default_timezone == :utc ? Time.now.utc : Time.now - now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML fixtures.delete('DEFAULTS') @@ -646,7 +645,9 @@ module ActiveRecord # Resolve enums model_class.defined_enums.each do |name, values| - row[name] = values.fetch(row[name], row[name]) + if row.include?(name) + row[name] = values.fetch(row[name], row[name]) + end end # If STI is used, find the correct subclass for association reflection @@ -669,7 +670,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.type_for_attribute(fk_name).type + fk_type = reflection_class.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -826,12 +827,12 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - def before_setup + def before_setup # :nodoc: setup_fixtures super end - def after_teardown + def after_teardown # :nodoc: super teardown_fixtures end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 8a3c27e6da..0b35027b2b 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -16,8 +16,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index af816a278e..4d597a0ab1 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -47,18 +47,21 @@ module ActiveRecord binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end - if odd? - name = color(name, CYAN, true) - sql = color(sql, nil, true) - else - name = color(name, MAGENTA, true) - end + name = color(name, nil, true) + sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end - def odd? - @odd = !@odd + def sql_color(sql) + case sql + when /\s*\Ainsert/i then GREEN + when /\s*\Aselect/i then BLUE + when /\s*\Aupdate/i then YELLOW + when /\s*\Adelete/i then RED + when /transaction\s*\Z/i then CYAN + else MAGENTA + end end def logger diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5af788bc10..b5b91451c7 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -138,10 +138,13 @@ module ActiveRecord # <tt>:name</tt>, <tt>:unique</tt> (e.g. # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> # (e.g. <tt>{ order: { name: :desc } }</tt>). - # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index - # specified by +column_name+. + # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index + # specified by +column_names+. # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default a integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. # # == Irreversible transformations # @@ -638,7 +641,8 @@ module ActiveRecord unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - if [:rename_table, :add_foreign_key].include?(method) + if [:rename_table, :add_foreign_key].include?(method) || + (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) arguments[1] = proper_table_name(arguments.second, table_name_options) end end @@ -713,7 +717,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 36256415df..dcc2362397 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -72,7 +72,7 @@ module ActiveRecord [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, - :change_column_default, :add_reference, :remove_reference, :transaction, + :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, :change_column, :execute, :remove_columns, :change_column_null, :add_foreign_key, :remove_foreign_key @@ -151,19 +151,31 @@ module ActiveRecord end def invert_remove_index(args) - table, options = *args - - unless options && options.is_a?(Hash) && options[:column] - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + table, options_or_column = *args + if (options = options_or_column).is_a?(Hash) + unless options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end + options = options.dup + [:add_index, [table, options.delete(:column), options]] + elsif (column = options_or_column).present? + [:add_index, [table, column]] end - - options = options.dup - [:add_index, [table, options.delete(:column), options]] end alias :invert_add_belongs_to :invert_add_reference alias :invert_remove_belongs_to :invert_remove_reference + def invert_change_column_default(args) + table, column, options = *args + + unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to) + raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option." + end + + [:change_column_default, [table, column, from: options[:to], to: options[:from]]] + end + def invert_change_column_null(args) args[2] = !args[2] [:change_column_null, args] @@ -184,6 +196,16 @@ module ActiveRecord [:remove_foreign_key, [from_table, options]] end + def invert_remove_foreign_key(args) + from_table, to_table, remove_options = args + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + + reversed_args = [from_table, to_table] + reversed_args << remove_options if remove_options + + [:add_foreign_key, reversed_args] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 3674f672cb..5a6f42ba09 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -310,6 +310,7 @@ module ActiveRecord def load_schema! @columns_hash = connection.schema_cache.columns_hash(table_name) @columns_hash.each do |name, column| + warn_if_deprecated_type(column) define_attribute( name, connection.lookup_cast_type_from_column(column), @@ -356,6 +357,28 @@ module ActiveRecord base.table_name end end + + def warn_if_deprecated_type(column) + return if attributes_to_define_after_schema_loads.key?(column.name) + if column.respond_to?(:oid) && column.sql_type.start_with?("point") + if column.array? + array_arguments = ", array: true" + else + array_arguments = "" + end + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + The behavior of the `:point` type will be changing in Rails 5.1 to + return a `Point` object, rather than an `Array`. If you'd like to + keep the old behavior, you can add this line to #{self.name}: + + attribute :#{column.name}, :legacy_point#{array_arguments} + + If you'd like the new behavior today, you can add this line: + + attribute :#{column.name}, :rails_5_1_point#{array_arguments} + WARNING + end + end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c942d0e265..c5a1488588 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -386,6 +386,9 @@ module ActiveRecord # 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] + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access existing_record = send(association_name) @@ -442,6 +445,9 @@ module ActiveRecord # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.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})" @@ -468,6 +474,9 @@ module ActiveRecord end attributes_collection.each do |attributes| + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access if attributes['id'].blank? @@ -552,7 +561,9 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + model = self.class._reflect_on_association(association_name).klass.name + raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", + model, 'id', record_id) end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 437ba31711..09c36d7b4d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -193,7 +193,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self)) + destroy || _raise_record_not_destroyed end # Returns an instance of the specified +klass+ with the attributes of the @@ -211,8 +211,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - changed_attributes = @changed_attributes if defined?(@changed_attributes) - became.instance_variable_set("@changed_attributes", changed_attributes || {}) + became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) @@ -549,5 +548,12 @@ module ActiveRecord def verify_readonly_attribute(name) raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) end + + def _raise_record_not_destroyed + @_association_destroy_exception ||= nil + raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self) + ensure + @_association_destroy_exception = nil + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..87a1988f2f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -6,7 +6,7 @@ module ActiveRecord delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all - delegate :find_each, :find_in_batches, to: :all + delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 5af64b717a..6dd54f9262 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,11 +16,11 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::QueryCache - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::ConnectionAdapters::ConnectionManagement config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, @@ -78,8 +78,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end @@ -156,8 +156,8 @@ end_warning ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.send(hook) do if ActiveRecord::Base.connected? - ActiveRecord::Base.clear_reloadable_connections! ActiveRecord::Base.clear_cache! + ActiveRecord::Base.clear_reloadable_connections! end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d168786e71..6a72d528b4 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -12,7 +12,7 @@ db_namespace = namespace :db do end end - desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.' + desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.' task :create => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end @@ -23,7 +23,7 @@ db_namespace = namespace :db do end end - desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.' + desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.' task :drop => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end @@ -42,15 +42,18 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration + db_namespace['_dump'].invoke end + # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false task :_dump do - case ActiveRecord::Base.schema_format - when :ruby then db_namespace["schema:dump"].invoke - when :sql then db_namespace["structure:dump"].invoke - else - raise "unknown schema format #{ActiveRecord::Base.schema_format}" + if ActiveRecord::Base.dump_schema_after_migration + case ActiveRecord::Base.schema_format + when :ruby then db_namespace["schema:dump"].invoke + when :sql then db_namespace["structure:dump"].invoke + else + raise "unknown schema format #{ActiveRecord::Base.schema_format}" + end end # Allow this task to be called as many times as required. An example is the # migrate:redo task, which calls other two internally that depend on this one. @@ -134,10 +137,7 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => [:environment, :load_config] do - db_namespace["drop"].invoke - db_namespace["setup"].invoke - end + task :reset => [ 'db:drop', 'db:setup' ] # desc "Retrieves the charset for the current environment's database" task :charset => [:environment, :load_config] do @@ -159,7 +159,7 @@ db_namespace = namespace :db do end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -171,17 +171,17 @@ db_namespace = namespace :db do end end - desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)' + desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)' task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] - desc 'Load the seed data from db/seeds.rb' + desc 'Loads the seed data from db/seeds.rb' task :seed do db_namespace['abort_if_pending_migrations'].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do - desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." + desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => [:environment, :load_config] do require 'active_record/fixtures' @@ -229,7 +229,7 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Create a db/schema.rb file that is portable against any DB supported by AR' + desc 'Creates a db/schema.rb file that is portable against any DB supported by AR' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') @@ -239,7 +239,7 @@ db_namespace = namespace :db do db_namespace['schema:dump'].reenable end - desc 'Load a schema.rb file into the database' + desc 'Loads a schema.rb file into the database' task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -249,7 +249,7 @@ db_namespace = namespace :db do end namespace :cache do - desc 'Create a db/schema_cache.dump file.' + desc 'Creates a db/schema_cache.dump file.' task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") @@ -259,7 +259,7 @@ db_namespace = namespace :db do open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } end - desc 'Clear a db/schema_cache.dump file.' + desc 'Clears a db/schema_cache.dump file.' task :clear => [:environment, :load_config] do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") FileUtils.rm(filename) if File.exist?(filename) @@ -269,7 +269,7 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' + desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config @@ -285,7 +285,7 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - desc "Recreate the databases from the structure.sql file" + desc "Recreates the databases from the structure.sql file" task :load => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7d37313058..e47b7b1ed9 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -15,6 +15,7 @@ module ActiveRecord VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS + include Enumerable include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder @@ -275,38 +276,52 @@ module ActiveRecord # Returns true if there are no records. def none? - if block_given? - to_a.none? { |*block_args| yield(*block_args) } - else - empty? - end + return super if block_given? + empty? end # Returns true if there are any records. def any? - if block_given? - to_a.any? { |*block_args| yield(*block_args) } - else - !empty? - end + return super if block_given? + !empty? end # Returns true if there is exactly one record. def one? - if block_given? - to_a.one? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.one? : size == 1 - end + return super if block_given? + limit_value ? to_a.one? : size == 1 end # Returns true if there is more than one record. def many? - if block_given? - to_a.many? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.many? : size > 1 - end + return super if block_given? + limit_value ? to_a.many? : size > 1 + end + + # Returns a cache key that can be used to identify the records fetched by + # this query. The cache key is built with a fingerprint of the sql query, + # the number of records matched by the query and a timestamp of the last + # updated record. When a new record comes to match the query, or any of + # the existing records is updated or deleted, the cache key changes. + # + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # + # You can also pass a custom timestamp column to fetch the timestamp of the + # last updated record. + # + # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) + # + # You can customize the strategy to generate the key on a per model basis + # overriding ActiveRecord::Base#collection_cache_key. + def cache_key(timestamp_column = :updated_at) + @cache_keys ||= {} + @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) end # Scope all queries to the current scope. @@ -652,6 +667,13 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end + protected + + def load_records(records) + @records = records + @loaded = true + end + private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..beb8fa511c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,3 +1,5 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches # Looping through a collection of records from the database @@ -122,24 +124,102 @@ module ActiveRecord end end + in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + yield batch.to_a + end + end + + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # 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| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # 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>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # + # This is especially useful if you want to work with the + # ActiveRecord::Relation object instead of the array of records, or if + # you want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and worker 2 + # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # option on each worker). + # + # # Let's process the next 2000 records + # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # 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 + # or a string. + # + # NOTE: You can't set the limit either, that's used to control the batch + # sizes. + def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + end + if logger && (arel.orders.present? || arel.taken.present?) logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - relation = relation.reorder(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(of) relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + batch_relation = relation + + loop do + if load + records = batch_relation.to_a + ids = records.map(&:id) + yielded_relation = self.where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = self.where(primary_key => ids) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + break if ids.empty? - yield records + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - break if records_size < batch_size + yield yielded_relation - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + break if ids.length < of + batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..153aae9584 --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + @of = of + @relation = relation + @begin_at = begin_at + @end_at = end_at + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| + relation.to_a.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.in_batches.destroy_all('age < 10') + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 402b317d9c..0f6015fa93 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -71,6 +71,7 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(*args) + return super if block_given? calculate(:sum, *args) end @@ -138,7 +139,7 @@ module ActiveRecord # # SELECT people.id, people.name FROM people # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] # - # Person.pluck('DISTINCT role') + # Person.distinct.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # @@ -161,6 +162,10 @@ module ActiveRecord end end + if loaded? && (column_names - @klass.column_names).empty? + return @records.pluck(*column_names) + end + if has_include?(column_names.first) construct_relation_for_association_calculations.pluck(*column_names) else @@ -190,7 +195,8 @@ module ActiveRecord def perform_calculation(operation, column_name) operation = operation.to_s.downcase - # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + # If #count is used with #distinct (i.e. `relation.distinct.count`) it is + # considered distinct. distinct = self.distinct_value if operation == "count" diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 86f2c30168..d75ec72b1a 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -18,7 +18,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub('::', '_'), delegate + const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate cache[klass] = delegate end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6020aa238f..009b2bad57 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -62,11 +62,8 @@ module ActiveRecord # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2) # # returns an Array of the required fields. def find(*args) - if block_given? - to_a.find(*args) { |*block_args| yield(*block_args) } - else - find_with_ids(*args) - end + return super if block_given? + find_with_ids(*args) end # Finds the first record matching the specified conditions. There @@ -88,7 +85,8 @@ module ActiveRecord def find_by!(arg, *args) where(arg, *args).take! rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" + raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", + @klass.name) end # Gives a record (or N records if a parameter is supplied) without any implied diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index dd8f0aa298..0b38666ce9 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -87,8 +87,8 @@ module ActiveRecord return if other.preload_values.empty? && other.includes_values.empty? if other.klass == relation.klass - relation.preload! other.preload_values unless other.preload_values.empty? - relation.includes! other.includes_values unless other.includes_values.empty? + relation.preload!(*other.preload_values) unless other.preload_values.empty? + relation.includes!(other.includes_values) unless other.includes_values.empty? else reflection = relation.klass.reflect_on_all_associations.find do |r| r.class_name == other.klass.name diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 43e9afe853..d26db7d4cf 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -52,7 +52,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first if key.include?('.') + key.split('.'.freeze).first if key.include?('.'.freeze) end end.compact end @@ -123,10 +123,10 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".") } + dot_notation = attributes.keys.select { |s| s.include?(".".freeze) } dot_notation.each do |key| - table_name, column_name = key.split(".") + table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index fd78db2e95..706c99c245 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -242,12 +242,9 @@ module ActiveRecord # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - if block_given? - to_a.select { |*block_args| yield(*block_args) } - else - raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn._select!(*fields) - end + return super if block_given? + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn._select!(*fields) end def _select!(*fields) # :nodoc: @@ -1001,15 +998,13 @@ module ActiveRecord end def arel_columns(columns) - if from_clause.value - columns - else - columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field - end + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value + arel_table[field] + elsif Symbol === field + connection.quote_table_name(field.to_s) + else + field end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c2567311bd..ba75ffa5a1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -119,9 +119,9 @@ module ActiveRecord end def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do + statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ':' # skip postgresql casts - $& # return the whole match + match # return the whole match elsif bind_vars.include?(match = $2.to_sym) replace_bind_variable(bind_vars[match]) else diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b5038104ac..cb47bf23f7 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,6 +1,5 @@ require 'active_record/scoping/default' require 'active_record/scoping/named' -require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 3590b8846e..a1adf8e3ee 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -100,6 +100,7 @@ module ActiveRecord end def build_default_scope(base_rel = relation) # :nodoc: + return if abstract_class? if !Base.is_a?(method(:default_scope).owner) # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..23dc6465af 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b0b86865fd..b3644bf569 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -37,8 +37,7 @@ module ActiveRecord end end - # Ignore saving events if we're in suppression mode. - def save!(*args) # :nodoc: + def create_or_update(*args) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 3dd6321a97..41f1c55c3c 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -41,7 +41,7 @@ module ActiveRecord association = klass._reflect_on_association(table_name) if association && !association.polymorphic? association_klass = association.klass - arel_table = association_klass.arel_table + arel_table = association_klass.arel_table.alias(table_name) else type_caster = TypeCaster::Connection.new(klass, table_name) association_klass = nil diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index d7da95c8a9..55f839444b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: @@ -55,19 +53,22 @@ module ActiveRecord when String ActiveRecord::Base.dump_schemas end + + args = ['-i', '-s', '-x', '-O', '-f', filename] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args << search_path.split(',').map do |part| + "--schema=#{part.strip}" + end.join(' ') end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration['database'] + run_cmd('pg_dump', args, 'dumping') File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) set_psql_env - Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = [ '-q', '-f', filename, configuration['database'] ] + run_cmd('psql', args, 'loading' ) end private @@ -93,6 +94,17 @@ module ActiveRecord ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + 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/transactions.rb b/activerecord/lib/active_record/transactions.rb index 311dacb449..887d7a5903 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -196,17 +196,16 @@ module ActiveRecord # automatically released. The following example demonstrates the problem: # # Model.connection.transaction do # BEGIN - # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 + # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released - # end # RELEASE savepoint active_record_1 + # end # RELEASE SAVEPOINT active_record_1 # # ^^^^ BOOM! database error! # end # # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods - # See ActiveRecord::Transactions::ClassMethods for detailed documentation. + # See the ConnectionAdapters::DatabaseStatements#transaction API docs. def transaction(options = {}, &block) - # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end @@ -319,8 +318,8 @@ module ActiveRecord end def before_committed! # :nodoc: - run_callbacks :before_commit_without_transaction_enrollment - run_callbacks :before_commit + _run_before_commit_without_transaction_enrollment_callbacks + _run_before_commit_callbacks end # Call the +after_commit+ callbacks. @@ -329,8 +328,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - run_callbacks :commit_without_transaction_enrollment - run_callbacks :commit + _run_commit_without_transaction_enrollment_callbacks + _run_commit_callbacks end ensure force_clear_transaction_record_state @@ -340,8 +339,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - run_callbacks :rollback - run_callbacks :rollback_without_transaction_enrollment + _run_rollback_callbacks + _run_rollback_without_transaction_enrollment_callbacks end ensure restore_transaction_record_state(force_restore_state) @@ -380,6 +379,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end protected diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 867b5f75c7..f200a92d10 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -8,7 +8,7 @@ module ActiveRecord end def type_cast_for_schema(value) - value.to_s + value.to_s.inspect end private diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index e227212827..34d96b19fe 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -76,4 +76,5 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/absence" require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb new file mode 100644 index 0000000000..2e19e6dc5c --- /dev/null +++ b/activerecord/lib/active_record/validations/absence.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module Validations + class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + return unless should_validate?(record) + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) + end + super + end + end + + module ClassMethods + # Validates that the specified attributes are not present (as defined by + # Object#present?). If the attribute is an association, the associated object + # is considered absent if it was marked for destruction. + # + # See ActiveModel::Validations::HelperMethods.validates_absence_of for more information. + def validates_absence_of(*attr_names) + validates_with AbsenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 5991fbad8e..69e048eef1 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -22,7 +22,10 @@ module ActiveRecord end module ClassMethods - # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + # Validates that the specified attributes match the length restrictions supplied. + # If the attribute is an association, records that are marked for destruction are not counted. + # + # See ActiveModel::Validations::HelperMethods.validates_length_of for more information. def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index a9b791397b..23a3985d35 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -1,18 +1,12 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: - def validate(record) + def validate_each(record, attribute, association_or_value) return unless should_validate?(record) - super - attributes.each do |attribute| - next unless record.class._reflect_on_association(attribute) - associated_records = Array.wrap(record.send(attribute)) - - # Superclass validates presence. Ensure present records aren't about to be destroyed. - if associated_records.present? && associated_records.all?(&:marked_for_destruction?) - record.errors.add(attribute, :blank, options) - end + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end + super end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5106f4e127..32d17a1392 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -17,7 +17,9 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? + if record.persisted? && finder_class.primary_key.to_s != attribute.to_s + relation = relation.where.not(finder_class.primary_key => record.id) + end relation = scope_relation(record, table, relation) relation = relation.merge(options[:conditions]) if options[:conditions] diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 57eb5d0e18..f0fd95ac16 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class ActiveSchemaTest < ActiveRecord::TestCase +class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase include ConnectionHelper def setup diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 345122b1ad..98d44315dd 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class MysqlCaseSensitivityTest < ActiveRecord::TestCase +class MysqlCaseSensitivityTest < ActiveRecord::MysqlTestCase class CollationTest < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb index c8dd49d00a..f2117a97e6 100644 --- a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class CharsetCollationTest < ActiveRecord::TestCase +class MysqlCharsetCollationTest < ActiveRecord::MysqlTestCase include SchemaDumpingHelper self.use_transactional_tests = false diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 9903cd3c0b..ddbc007b87 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' require 'support/ddl_helper' -class MysqlConnectionTest < ActiveRecord::TestCase +class MysqlConnectionTest < ActiveRecord::MysqlTestCase include ConnectionHelper include DdlHelper diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb index ae190b728d..743f6436e4 100644 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class MysqlConsistencyTest < ActiveRecord::TestCase +class MysqlConsistencyTest < ActiveRecord::MysqlTestCase self.use_transactional_tests = false class Consistency < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb index f4e7a3ef0a..ef8ee0a6e3 100644 --- a/activerecord/test/cases/adapters/mysql/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql/enum_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class MysqlEnumTest < ActiveRecord::TestCase +class MysqlEnumTest < ActiveRecord::MysqlTestCase class EnumTest < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 48ceef365e..b804cb45b9 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -4,7 +4,7 @@ require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class MysqlAdapterTest < ActiveRecord::TestCase + class MysqlAdapterTest < ActiveRecord::MysqlTestCase include DdlHelper def setup diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index a2206153e9..2024aa36ab 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -1,21 +1,29 @@ require "cases/helper" -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter - class QuotingTest < ActiveRecord::TestCase - def setup - @conn = ActiveRecord::Base.connection - end +class MysqlQuotingTest < ActiveRecord::MysqlTestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_true + assert_equal 1, @conn.type_cast(true) + end - def test_type_cast_true - assert_equal 1, @conn.type_cast(true) - end + def test_type_cast_false + assert_equal 0, @conn.type_cast(false) + end + + def test_quoted_date_precision_for_gte_564 + @conn.stubs(:full_version).returns('5.6.4') + @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) + t = Time.now.change(usec: 1) + assert_match(/\.000001\z/, @conn.quoted_date(t)) + end - def test_type_cast_false - assert_equal 0, @conn.type_cast(false) - end - end - end + def test_quoted_date_precision_for_lt_564 + @conn.stubs(:full_version).returns('5.6.3') + @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) + t = Time.now.change(usec: 1) + assert_no_match(/\.000001\z/, @conn.quoted_date(t)) end end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index ec1c394f40..4ea1d9ad36 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -1,29 +1,29 @@ require "cases/helper" -class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values -end +# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with +# reserved word names (ie: group, order, values, etc...) +class MysqlReservedWordTest < ActiveRecord::MysqlTestCase + class Group < ActiveRecord::Base + Group.table_name = 'group' + belongs_to :select + has_one :values + end -class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups -end + class Select < ActiveRecord::Base + Select.table_name = 'select' + has_many :groups + end -class Values < ActiveRecord::Base - Values.table_name = 'values' -end + class Values < ActiveRecord::Base + Values.table_name = 'values' + end -class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups -end + class Distinct < ActiveRecord::Base + Distinct.table_name = 'distinct' + has_and_belongs_to_many :selects + has_many :values, :through => :groups + end -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index b7f9c2ce84..2e18f609fd 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -4,7 +4,7 @@ require 'models/comment' module ActiveRecord module ConnectionAdapters - class MysqlSchemaTest < ActiveRecord::TestCase + class MysqlSchemaTest < ActiveRecord::MysqlTestCase fixtures :posts def setup diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb index 3ca2917ca4..a3d5110032 100644 --- a/activerecord/test/cases/adapters/mysql/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql/sp_test.rb @@ -1,11 +1,11 @@ require "cases/helper" require 'models/topic' -class StoredProcedureTest < ActiveRecord::TestCase +class StoredProcedureTest < ActiveRecord::MysqlTestCase fixtures :topics # Test that MySQL allows multiple results for stored procedures - if Mysql.const_defined?(:CLIENT_MULTI_RESULTS) + if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS) def test_multi_results_from_find_by_sql topics = Topic.find_by_sql 'CALL topics();' assert_equal 1, topics.size diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb index 1ddb1b91c9..25b28de7f0 100644 --- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql/sql_types_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class SqlTypesTest < ActiveRecord::TestCase +class MysqlSqlTypesTest < ActiveRecord::MysqlTestCase def test_binary_types assert_equal 'varbinary(64)', type_to_sql(:binary, 64) assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb index 209a0cf464..6be36566de 100644 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -1,23 +1,19 @@ require 'cases/helper' -module ActiveRecord::ConnectionAdapters - class MysqlAdapter - class StatementPoolTest < ActiveRecord::TestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] +class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end + Process.waitpid pid + assert $?.success?, 'process should exit successfully' end end end diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb index 0e5b0e8aec..99df6d6cba 100644 --- a/activerecord/test/cases/adapters/mysql/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql/table_options_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class MysqlTableOptionsTest < ActiveRecord::TestCase +class MysqlTableOptionsTest < ActiveRecord::MysqlTestCase include SchemaDumpingHelper def setup diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb index e9edc53f93..ed9398a918 100644 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class UnsignedTypeTest < ActiveRecord::TestCase +class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 0ea556d4fa..6558d60aa1 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class ActiveSchemaTest < ActiveRecord::TestCase +class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase include ConnectionHelper def setup diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index 5e8065d80d..abdf3dbf5b 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -4,7 +4,7 @@ require 'models/topic' module ActiveRecord module ConnectionAdapters class Mysql2Adapter - class BindParameterTest < ActiveRecord::TestCase + class BindParameterTest < ActiveRecord::Mysql2TestCase fixtures :topics def test_update_question_marks diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 0d81dd6eee..8575df9e43 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class Mysql2BooleanTest < ActiveRecord::TestCase +class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase self.use_transactional_tests = false class BooleanType < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index ccf3d84a44..963116f08a 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class Mysql2CaseSensitivityTest < ActiveRecord::TestCase +class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase class CollationTest < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index c8dd49d00a..4fd34def15 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class CharsetCollationTest < ActiveRecord::TestCase +class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase include SchemaDumpingHelper self.use_transactional_tests = false diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index c4e0278c89..000bcadebe 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class MysqlConnectionTest < ActiveRecord::TestCase +class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase include ConnectionHelper fixtures :comments diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 6dd9a5ec87..bd732b5eca 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class Mysql2EnumTest < ActiveRecord::TestCase +class Mysql2EnumTest < ActiveRecord::Mysql2TestCase class EnumTest < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 2b01d941b8..4fc7414b18 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -5,7 +5,7 @@ require 'models/computer' module ActiveRecord module ConnectionAdapters class Mysql2Adapter - class ExplainTest < ActiveRecord::TestCase + class ExplainTest < ActiveRecord::Mysql2TestCase fixtures :developers def test_explain_for_one_query diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb new file mode 100644 index 0000000000..2de7e1b526 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb @@ -0,0 +1,21 @@ +require "cases/helper" + +class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test 'quoted date precision for gte 5.6.4' do + @connection.stubs(:full_version).returns('5.6.4') + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + t = Time.now.change(usec: 1) + assert_match(/\.000001\z/, @connection.quoted_date(t)) + end + + test 'quoted date precision for lt 5.6.4' do + @connection.stubs(:full_version).returns('5.6.3') + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + t = Time.now.change(usec: 1) + assert_no_match(/\.000001\z/, @connection.quoted_date(t)) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 799e60a683..ffb4e2c5cf 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -1,29 +1,29 @@ require "cases/helper" -class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values -end +# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with +# reserved word names (ie: group, order, values, etc...) +class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase + class Group < ActiveRecord::Base + Group.table_name = 'group' + belongs_to :select + has_one :values + end -class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups -end + class Select < ActiveRecord::Base + Select.table_name = 'select' + has_many :groups + end -class Values < ActiveRecord::Base - Values.table_name = 'values' -end + class Values < ActiveRecord::Base + Values.table_name = 'values' + end -class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups -end + class Distinct < ActiveRecord::Base + Distinct.table_name = 'distinct' + has_and_belongs_to_many :selects + has_many :values, :through => :groups + end -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 417ccf6d11..396f235e77 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,47 +1,42 @@ require "cases/helper" -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter - class SchemaMigrationsTest < ActiveRecord::TestCase - def test_renaming_index_on_foreign_key - connection.add_index "engines", "car_id" - connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" - - connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") - assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) - ensure - connection.remove_foreign_key :engines, name: "fk_engines_cars" - end - - def test_initializes_schema_migrations_for_encoding_utf8mb4 - smtn = ActiveRecord::Migrator.schema_migrations_table_name - connection.drop_table smtn, if_exists: true - - database_name = connection.current_database - database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") - - original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] - original_collation = database_info["DEFAULT_COLLATION_NAME"] - - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - - connection.initialize_schema_migrations_table - - assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) - ensure - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") - end - - private - def connection - @connection ||= ActiveRecord::Base.connection - end - - def execute(sql) - connection.execute(sql) - end - end - end +class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase + def test_renaming_index_on_foreign_key + connection.add_index "engines", "car_id" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" + + connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") + assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) + ensure + connection.remove_foreign_key :engines, name: "fk_engines_cars" + end + + def test_initializes_schema_migrations_for_encoding_utf8mb4 + smtn = ActiveRecord::Migrator.schema_migrations_table_name + connection.drop_table smtn, if_exists: true + + database_name = connection.current_database + database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") + + original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] + original_collation = database_info["DEFAULT_COLLATION_NAME"] + + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") + + connection.initialize_schema_migrations_table + + limit = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN + assert connection.column_exists?(smtn, :version, :string, limit: limit) + ensure + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") + end + + private + def connection + @connection ||= ActiveRecord::Base.connection + end + + def execute(sql) + connection.execute(sql) end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 47707b7d4f..880a2123d2 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -4,7 +4,7 @@ require 'models/comment' module ActiveRecord module ConnectionAdapters - class Mysql2SchemaTest < ActiveRecord::TestCase + class Mysql2SchemaTest < ActiveRecord::Mysql2TestCase fixtures :posts def setup diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index 1ddb1b91c9..ae505d29c9 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class SqlTypesTest < ActiveRecord::TestCase +class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase def test_binary_types assert_equal 'varbinary(64)', type_to_sql(:binary, 64) assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb index 0e5b0e8aec..af121ee7d9 100644 --- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class MysqlTableOptionsTest < ActiveRecord::TestCase +class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase include SchemaDumpingHelper def setup diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index e9edc53f93..9e06db2519 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class UnsignedTypeTest < ActiveRecord::TestCase +class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 3808db5141..dc7ba314c6 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlActiveSchemaTest < ActiveRecord::TestCase +class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 6edbd9c3a6..380a90d765 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,10 +1,9 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlArrayTest < ActiveRecord::TestCase +class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper include InTimeZone - OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' @@ -212,8 +211,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_quoting_non_standard_delimiters strings = ["hello,", "world;"] - comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') - semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ';') assert_equal %({"hello,",world;}), comma_delim.serialize(strings) assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 1a5ff4316c..6f72fa6e0f 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlBitStringTest < ActiveRecord::TestCase +class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 16db5ab83d..b6bb1929e6 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlByteaTest < ActiveRecord::TestCase +class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase class ByteaDataType < ActiveRecord::Base self.table_name = 'bytea_data_type' end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index 5a9796887c..bc12df668d 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' module ActiveRecord class Migration - class PGChangeSchemaTest < ActiveRecord::TestCase + class PGChangeSchemaTest < ActiveRecord::PostgreSQLTestCase attr_reader :connection def setup diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 6cb11d17b4..52f2a0096c 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -3,8 +3,8 @@ require "ipaddr" module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter - class CidrTest < ActiveRecord::TestCase + class PostgreSQLAdapter < AbstractAdapter + class CidrTest < ActiveRecord::PostgreSQLTestCase test "type casting IPAddr for database" do type = OID::Cidr.new ip = IPAddr.new("255.0.0.0/8") diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index f706847890..bd62041e79 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlCitextTest < ActiveRecord::TestCase + class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Citext < ActiveRecord::Base self.table_name = 'citexts' diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb index 17ef5f304c..8470329c35 100644 --- a/activerecord/test/cases/adapters/postgresql/collation_test.rb +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlCollationTest < ActiveRecord::TestCase +class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper def setup diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 16e3f90a47..1de87e5f01 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -40,7 +40,7 @@ end # "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." # To take full advantage of composite types, we suggest you register your own +OID::Type+. # See PostgresqlCompositeWithCustomOIDTest -class PostgresqlCompositeTest < ActiveRecord::TestCase +class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior def test_column @@ -77,7 +77,7 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end -class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior class FullAddressType < ActiveRecord::Type::Value diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 55ad76c8c0..820d41e13b 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' module ActiveRecord - class PostgresqlConnectionTest < ActiveRecord::TestCase + class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class NonExistentTable < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 2c14252ae4..232c25cb3b 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -11,7 +11,7 @@ end class PostgresqlLtree < ActiveRecord::Base end -class PostgresqlDataTypeTest < ActiveRecord::TestCase +class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false def setup @@ -69,7 +69,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end end -class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase +class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase include DdlHelper setup do diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 26e064c937..6102ddacd1 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlDomainTest < ActiveRecord::TestCase +class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlDomain < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index ed084483bc..6816a6514b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlEnumTest < ActiveRecord::TestCase +class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlEnum < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 6ffb4c9f33..4d0fd640aa 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -2,25 +2,19 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers - def test_explain_for_one_query - explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(QUERY PLAN), explain - end + def test_explain_for_one_query + explain = Developer.where(:id => 1).explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain + assert_match %(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 %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(:id => 1).includes(:audit_logs).explain + assert_match %(QUERY PLAN), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $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/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 06d427f464..9cfc133308 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase +class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false class EnableHstore < ActiveRecord::Migration diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index b83063c94e..bde7513339 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlFullTextTest < ActiveRecord::TestCase +class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Tsvector < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 41e9572907..0baf985654 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -2,11 +2,19 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlPointTest < ActiveRecord::TestCase +class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper - class PostgresqlPoint < ActiveRecord::Base; end + class PostgresqlPoint < ActiveRecord::Base + attribute :x, :rails_5_1_point + attribute :y, :rails_5_1_point + attribute :z, :rails_5_1_point + attribute :array_of_points, :rails_5_1_point, array: true + attribute :legacy_x, :legacy_point + attribute :legacy_y, :legacy_point + attribute :legacy_z, :legacy_point + end def setup @connection = ActiveRecord::Base.connection @@ -14,11 +22,27 @@ class PostgresqlPointTest < ActiveRecord::TestCase t.point :x t.point :y, default: [12.2, 13.3] t.point :z, default: "(14.4,15.5)" + t.point :array_of_points, array: true + t.point :legacy_x + t.point :legacy_y, default: [12.2, 13.3] + t.point :legacy_z, default: "(14.4,15.5)" + end + @connection.create_table('deprecated_points') do |t| + t.point :x end end teardown do @connection.drop_table 'postgresql_points', if_exists: true + @connection.drop_table 'deprecated_points', if_exists: true + end + + class DeprecatedPoint < ActiveRecord::Base; end + + def test_deprecated_legacy_type + assert_deprecated do + DeprecatedPoint.new + end end def test_column @@ -32,11 +56,11 @@ class PostgresqlPointTest < ActiveRecord::TestCase end def test_default - assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] - assert_equal [12.2, 13.3], PostgresqlPoint.new.y + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y'] + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y - assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] - assert_equal [14.4, 15.5], PostgresqlPoint.new.z + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z'] + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z end def test_schema_dumping @@ -49,27 +73,100 @@ class PostgresqlPointTest < ActiveRecord::TestCase def test_roundtrip PostgresqlPoint.create! x: [10, 25.2] record = PostgresqlPoint.first - assert_equal [10, 25.2], record.x + assert_equal ActiveRecord::Point.new(10, 25.2), record.x - record.x = [1.1, 2.2] + record.x = ActiveRecord::Point.new(1.1, 2.2) record.save! assert record.reload - assert_equal [1.1, 2.2], record.x + assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x end def test_mutation - p = PostgresqlPoint.create! x: [10, 20] + p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20) + + p.x.y = 25 + p.save! + p.reload + + assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x + assert_not p.changed? + end + + def test_array_assignment + p = PostgresqlPoint.new(x: [1, 2]) + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_string_assignment + p = PostgresqlPoint.new(x: "(1, 2)") + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_array_of_points_round_trip + expected_value = [ + ActiveRecord::Point.new(1, 2), + ActiveRecord::Point.new(2, 3), + ActiveRecord::Point.new(3, 4), + ] + p = PostgresqlPoint.new(array_of_points: expected_value) + + assert_equal expected_value, p.array_of_points + p.save! + p.reload + assert_equal expected_value, p.array_of_points + end + + def test_legacy_column + column = PostgresqlPoint.columns_hash["legacy_x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.array? + + type = PostgresqlPoint.type_for_attribute("legacy_x") + assert_not type.binary? + end + + def test_legacy_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y'] + assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z'] + assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z + end + + def test_legacy_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"legacy_x"$}, output + assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_legacy_roundtrip + PostgresqlPoint.create! legacy_x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.legacy_x + + record.legacy_x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.legacy_x + end + + def test_legacy_mutation + p = PostgresqlPoint.create! legacy_x: [10, 20] - p.x[1] = 25 + p.legacy_x[1] = 25 p.save! p.reload - assert_equal [10.0, 25.0], p.x + assert_equal [10.0, 25.0], p.legacy_x assert_not p.changed? end end -class PostgresqlGeometricTest < ActiveRecord::TestCase +class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase class PostgresqlGeometric < ActiveRecord::Base; end setup do diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index ad9dd311a6..6a2d501646 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlHstoreTest < ActiveRecord::TestCase + class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Hstore < ActiveRecord::Base self.table_name = 'hstores' diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index d9d7832094..bfda933fa4 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlInfinityTest < ActiveRecord::TestCase +class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase include InTimeZone class PostgresqlInfinity < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index 679a0fc7b3..b4e55964b9 100644 --- a/activerecord/test/cases/adapters/postgresql/integer_test.rb +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require "active_support/core_ext/numeric/bytes" -class PostgresqlIntegerTest < ActiveRecord::TestCase +class PostgresqlIntegerTest < ActiveRecord::PostgreSQLTestCase class PgInteger < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 6878516aeb..f242f32496 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -188,7 +188,7 @@ module PostgresqlJSONSharedTestCases end end -class PostgresqlJSONTest < ActiveRecord::TestCase +class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type @@ -196,7 +196,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end end -class PostgresqlJSONBTest < ActiveRecord::TestCase +class PostgresqlJSONBTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ce0ad16557..56516c82b4 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlLtreeTest < ActiveRecord::TestCase +class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Ltree < ActiveRecord::Base self.table_name = 'ltrees' diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index cedd399380..c031178479 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlMoneyTest < ActiveRecord::TestCase +class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlMoney < ActiveRecord::Base; end @@ -56,7 +56,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase def test_schema_dumping output = dump_table_schema("postgresql_moneys") assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output - assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150\.55$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: "150\.55"$}, output end def test_create_and_update_money diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 033695518e..fe6ee4e2d9 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlNetworkTest < ActiveRecord::TestCase +class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlNetworkAddress < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index d8e01e3b89..ba7e7dc9a3 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlNumberTest < ActiveRecord::TestCase +class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase class PostgresqlNumber < ActiveRecord::Base; end setup do diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 9a1b889d4d..6e6850c4a9 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -4,7 +4,7 @@ require 'support/connection_helper' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapterTest < ActiveRecord::TestCase + class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase include DdlHelper include ConnectionHelper diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index e4420d9d13..5e6f4dbbb8 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -4,7 +4,7 @@ require 'ipaddr' module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::PostgreSQLTestCase def setup @conn = ActiveRecord::Base.connection end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index bbf96278b0..02b1083430 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,12 +1,12 @@ require "cases/helper" require 'support/connection_helper' -if ActiveRecord::Base.connection.supports_ranges? +if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base self.table_name = "postgresql_ranges" end - class PostgresqlRangeTest < ActiveRecord::TestCase + class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index 7200ed2771..c895ab9db5 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/connection_helper' -class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index f507328868..bd64bae308 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlRenameTableTest < ActiveRecord::TestCase +class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.create_table :before_rename, force: true diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 359a45bbd1..fa6584eae5 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class SchemaThing < ActiveRecord::Base end -class SchemaAuthorizationTest < ActiveRecord::TestCase +class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false TABLE_NAME = 'schema_things' diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index f925dcad97..35d5581aa7 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'models/default' require 'support/schema_dumping_helper' -class SchemaTest < ActiveRecord::TestCase +class SchemaTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' @@ -441,7 +441,7 @@ class SchemaTest < ActiveRecord::TestCase end end -class SchemaForeignKeyTest < ActiveRecord::TestCase +class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do @@ -466,7 +466,7 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase end end -class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase +class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" @@ -480,6 +480,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase @connection.create_table "defaults" do |t| t.text "text_col", default: "some value" t.string "string_col", default: "some value" + t.decimal "decimal_col", default: "3.14159265358979323846" end Default.reset_column_information end @@ -498,6 +499,10 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" end + def test_decimal_defaults_in_new_schema_when_overriding_domain + assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + end + def test_bpchar_defaults_in_new_schema_when_overriding_domain @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" Default.reset_column_information diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 458a8dae6c..7d30db247b 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlSerialTest < ActiveRecord::TestCase +class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlSerial < ActiveRecord::Base; end @@ -30,7 +30,7 @@ class PostgresqlSerialTest < ActiveRecord::TestCase end end -class PostgresqlBigSerialTest < ActiveRecord::TestCase +class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlBigSerial < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index 1497b0abc7..5aab246c99 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end end - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::PostgreSQLTestCase if Process.respond_to?(:fork) def test_cache_is_per_pid cache = StatementPool.new nil, 10 diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index a639f98272..4c4866b46b 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'models/developer' require 'models/topic' -class PostgresqlTimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase class PostgresqlTimestampWithZone < ActiveRecord::Base; end self.use_transactional_tests = false @@ -43,7 +43,7 @@ class PostgresqlTimestampTest < ActiveRecord::TestCase end end -class TimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase fixtures :topics def test_group_by_date diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index c0907b8f21..77a99ca778 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlTypeLookupTest < ActiveRecord::TestCase +class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 3fdb6888d9..095c1826e5 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' +require 'active_record/connection_adapters/postgresql/utils' -class PostgreSQLUtilsTest < ActiveSupport::TestCase +class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils @@ -20,7 +21,7 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase end end -class PostgreSQLNameTest < ActiveSupport::TestCase +class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name test "represents itself as schema.name" do diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index e9379a1019..7127d69e9e 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -11,7 +11,7 @@ module PostgresqlUUIDHelper end end -class PostgresqlUUIDTest < ActiveRecord::TestCase +class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -135,7 +135,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end end -class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase +class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -210,7 +210,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end end -class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase +class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -244,7 +244,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase end end -class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase +class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper class UuidPost < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb index 8a8e1d3b17..2dd6ec5fe6 100644 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require "cases/view_test" -class UpdateableViewTest < ActiveRecord::TestCase +class UpdateableViewTest < ActiveRecord::PostgreSQLTestCase fixtures :books class PrintedBook < ActiveRecord::Base @@ -46,8 +46,9 @@ class UpdateableViewTest < ActiveRecord::TestCase end end -if ActiveRecord::Base.connection.supports_materialized_views? -class MaterializedViewTest < ActiveRecord::TestCase +if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && + ActiveRecord::Base.connection.supports_materialized_views? +class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase include ViewBehavior private diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index b097deb2f4..add32699fa 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -class PostgresqlXMLTest < ActiveRecord::TestCase +class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class XmlDataType < ActiveRecord::Base self.table_name = 'xml_data_type' diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index 39e4620bc9..58a9469ce5 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class SQLite3CollationTest < ActiveRecord::TestCase +class SQLite3CollationTest < ActiveRecord::SQLite3TestCase include SchemaDumpingHelper def setup diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 13b754d226..34e3b2e023 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class CopyTableTest < ActiveRecord::TestCase +class CopyTableTest < ActiveRecord::SQLite3TestCase fixtures :customers def setup diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 7d66c44798..2aec322582 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -5,7 +5,7 @@ require 'models/computer' module ActiveRecord module ConnectionAdapters class SQLite3Adapter - class ExplainTest < ActiveRecord::TestCase + class ExplainTest < ActiveRecord::SQLite3TestCase fixtures :developers def test_explain_for_one_query diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 243f65df98..87a892db37 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -6,7 +6,7 @@ require 'securerandom' module ActiveRecord module ConnectionAdapters class SQLite3Adapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::SQLite3TestCase def setup @conn = Base.sqlite3_connection :database => ':memory:', :adapter => 'sqlite3', diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 6be9795603..7996e7ad50 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -5,7 +5,7 @@ require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class SQLite3AdapterTest < ActiveRecord::TestCase + class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase include DdlHelper self.use_transactional_tests = false diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index deedf67c8e..887dcfc96c 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -3,7 +3,7 @@ require 'models/owner' module ActiveRecord module ConnectionAdapters - class SQLite3CreateFolder < ActiveRecord::TestCase + class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase def test_sqlite_creates_directory Dir.mktmpdir do |dir| dir = Pathname.new(dir) diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index fd0044ac05..ef324183a7 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class SQLite3Adapter - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid @@ -22,4 +22,3 @@ module ActiveRecord::ConnectionAdapters end 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 ba90c61d65..0d2e2264e3 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -19,6 +19,11 @@ require 'models/invoice' require 'models/line_item' require 'models/column' require 'models/record' +require 'models/admin' +require 'models/admin/user' +require 'models/ship' +require 'models/treasure' +require 'models/parrot' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -31,6 +36,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute + assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm } + end + def test_belongs_to_does_not_use_order_by ActiveRecord::SQLCounter.clear_log Client.find(3).firm @@ -85,7 +94,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -102,7 +111,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -147,6 +156,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) } end + def test_raises_type_mismatch_with_namespaced_class + assert_nil defined?(Region), "This test requires that there is no top-level Region class" + + ActiveRecord::Base.connection.instance_eval do + create_table(:admin_regions) { |t| t.string :name } + add_column :admin_users, :region_id, :integer + end + Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) } + Admin.const_set "Region", Class.new(ActiveRecord::Base) + + e = assert_raise(ActiveRecord::AssociationTypeMismatch) { + Admin::RegionalUser.new(region: 'wrong value') + } + assert_match(/^Region\([^)]+\) expected, got String\([^)]+\)$/, e.message) + ensure + Admin.send :remove_const, "Region" if Admin.const_defined?("Region") + Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser") + + ActiveRecord::Base.connection.instance_eval do + remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id) + drop_table :admin_regions, if_exists: true + end + end + def test_natural_assignment apple = Firm.create("name" => "Apple") citibank = Account.create("credit_limit" => 10) @@ -263,7 +296,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.find(3) client.firm = nil client.save - assert_nil client.firm(true) + client.association(:firm).reload + assert_nil client.firm assert_nil client.client_of end @@ -271,7 +305,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) client.firm_with_primary_key = nil client.save - assert_nil client.firm_with_primary_key(true) + client.association(:firm_with_primary_key).reload + assert_nil client.firm_with_primary_key assert_nil client.client_of end @@ -288,9 +323,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_polymorphic_association_class sponsor = Sponsor.new assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable = Member.new :name => "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) @@ -311,6 +350,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase 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 + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless') + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = Treasure.new(name: 'Gold', ship: ship) + treasure.save + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = ship.treasures.first + treasure.destroy + end + end + def test_belongs_to_counter debate = Topic.create("title" => "debate") assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" @@ -525,7 +580,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm - assert_equal firm, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal firm, final_cut.firm end def test_assignment_before_child_saved_with_primary_key @@ -537,7 +593,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm_with_primary_key - assert_equal firm, final_cut.firm_with_primary_key(true) + final_cut.association(:firm_with_primary_key).reload + assert_equal firm, final_cut.firm_with_primary_key end def test_new_record_with_foreign_key_but_no_object @@ -1032,6 +1089,12 @@ 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/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ffbf60e390..8ed7b4e39f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1325,6 +1325,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preload with invalid argument" do + exception = assert_raises(ArgumentError) do + Author.preload(10).to_a + end + assert_equal('10 was not recognized for preload', exception.message) + end + + test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! 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 e584c94ad8..7718b29125 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 @@ -3,6 +3,7 @@ require 'models/developer' require 'models/computer' require 'models/project' require 'models/company' +require 'models/course' require 'models/customer' require 'models/order' require 'models/categorization' @@ -14,6 +15,7 @@ require 'models/tagging' require 'models/parrot' require 'models/person' require 'models/pirate' +require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' @@ -157,8 +159,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase jamis.projects << action_controller assert_equal 2, jamis.projects.size - assert_equal 2, jamis.projects(true).size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, jamis.projects.reload.size + assert_equal 2, action_controller.developers.reload.size end def test_adding_type_mismatch @@ -176,9 +178,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_from_the_project_fixed_timestamp @@ -192,9 +194,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis assert_equal updated_at, jamis.updated_at - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_multiple @@ -203,7 +205,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.push(Project.find(1), Project.find(2)) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_adding_a_collection @@ -212,7 +214,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.concat([Project.find(1), Project.find(2)]) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_adding_before_save @@ -227,7 +229,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase 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(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_saving_multiple_relationships @@ -391,8 +393,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.delete(active_record) assert_equal 1, david.projects.size - assert_equal 1, david.projects(true).size - assert_equal 2, active_record.developers(true).size + assert_equal 1, david.projects.reload.size + assert_equal 2, active_record.developers.reload.size end def test_deleting_array @@ -400,7 +402,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_deleting_all @@ -408,7 +410,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.clear assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_removing_associations_on_destroy @@ -434,7 +436,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 1, david.reload.projects.size - assert_equal 1, david.projects(true).size + assert_equal 1, david.projects.reload.size end def test_destroying_many @@ -450,7 +452,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 0, david.reload.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_destroy_all @@ -466,7 +468,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert david.projects.empty? - assert david.projects(true).empty? + assert david.projects.reload.empty? end def test_destroy_associations_destroys_multiple_associations @@ -482,11 +484,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.pirates(true).empty? + assert george.pirates.reload.empty? join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.treasures(true).empty? + assert george.treasures.reload.empty? end def test_associations_with_conditions @@ -654,7 +656,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_habtm_respects_select - categories(:technology).select_testing_posts(true).each do |o| + categories(:technology).select_testing_posts.reload.each do |o| assert_respond_to o, :correctness_marker end assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker @@ -726,7 +728,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) - developer.projects(true) + developer.projects.reload assert_queries(0) do developer.project_ids developer.project_ids @@ -917,4 +919,20 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase DeveloperWithSymbolClassName.new end end + + def test_association_force_reload_with_only_true_is_deprecated + developer = Developer.find(1) + + assert_deprecated { developer.projects(true) } + end + + def test_alternate_database + professor = Professor.create(name: "Plum") + course = Course.create(name: "Forensics") + assert_equal 0, professor.courses.count + assert_nothing_raised do + professor.courses << course + end + assert_equal 1, professor.courses.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 2c4e2a875c..f487065d9d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/ship_part' +require 'models/treasure' +require 'models/parrot' require 'models/tyre' require 'models/subscriber' require 'models/subscription' @@ -704,7 +706,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection - assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db assert_equal natural, companies(:first_firm).clients_of_firm.last end @@ -759,7 +761,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) assert_equal 4, companies(:first_firm).clients_of_firm.size - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_transactions_when_adding_to_persisted @@ -771,7 +773,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert !companies(:first_firm).clients_of_firm(true).include?(good) + assert !companies(:first_firm).clients_of_firm.reload.include?(good) end def test_transactions_when_adding_to_new_record @@ -903,12 +905,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert new_client.persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert_equal new_client, companies(:first_firm).clients_of_firm(true).last + assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last end def test_create_many companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_create_followed_by_save_does_not_load_target @@ -921,7 +923,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) assert_equal 1, companies(:first_firm).clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_deleting_before_save @@ -932,6 +934,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, new_firm.clients_of_firm.size end + def test_has_many_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless', treasures_count: 10) + + assert_not ship.treasures.instance_variable_get('@association').send(:has_cached_counter?) + + # Count should come from sql count() of treasures rather than treasures_count attribute + assert_equal ship.treasures.size, 0 + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.create(name: 'Gold') + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.destroy_all + end + end + def test_deleting_updates_counter_cache topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count @@ -1058,7 +1079,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_delete_all @@ -1079,7 +1100,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.reset companies(:first_firm).clients_of_firm.delete_all assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_transaction_when_deleting_persisted @@ -1093,7 +1114,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnDestroy end - assert_equal [good, bad], companies(:first_firm).clients_of_firm(true) + assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload end def test_transaction_when_deleting_new_record @@ -1113,7 +1134,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should not be destroyed since the association is not dependent. @@ -1149,7 +1170,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size - assert_equal 0, firm.dependent_clients_of_firm(true).size + assert_equal 0, firm.dependent_clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. @@ -1182,7 +1203,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.exclusively_dependent_clients_of_firm.clear assert_equal 0, firm.exclusively_dependent_clients_of_firm.size - assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size + assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size # no destroy-filters should have been called assert_equal [], Client.destroyed_client_ids[firm.id] @@ -1231,7 +1252,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # break the vanilla firm_id foreign key assert_equal 3, firm.clients.count firm.clients.first.update_columns(firm_id: nil) - assert_equal 2, firm.clients(true).count + assert_equal 2, firm.clients.reload.count assert_equal 2, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first firm = Firm.first @@ -1257,7 +1278,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size end def test_deleting_a_item_which_is_not_in_the_collection @@ -1265,7 +1286,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) assert_equal 2, companies(:first_firm).clients_of_firm.size - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size assert_equal 2, summit.client_of end @@ -1303,7 +1324,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_fixnum_id @@ -1314,7 +1335,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_string_id @@ -1325,7 +1346,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_a_collection @@ -1338,7 +1359,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroy_all @@ -1349,7 +1370,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" - assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" + assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh" end def test_dependence @@ -1426,6 +1447,26 @@ 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') @@ -1518,7 +1559,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert_equal [good], companies(:first_firm).clients_of_firm(true) + assert_equal [good], companies(:first_firm).clients_of_firm.reload end def test_transactions_when_replacing_on_new_record @@ -1534,7 +1575,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations company = companies(:first_firm) - company.clients(true) + company.clients.reload assert_queries(0) do company.client_ids company.client_ids @@ -1588,7 +1629,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ''] firm.save! - assert_equal 2, firm.clients(true).size + assert_equal 2, firm.clients.reload.size assert_equal true, firm.clients.include?(companies(:second_client)) end @@ -2252,4 +2293,48 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + + def test_association_force_reload_with_only_true_is_deprecated + company = Company.find(1) + + assert_deprecated { company.clients_of_firm(true) } + end + + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base + self.table_name = "authors" + has_many :posts_with_error_destroying, + class_name: "PostWithErrorDestroying", + foreign_key: :author_id, + dependent: :destroy + end + + class PostWithErrorDestroying < ActiveRecord::Base + self.table_name = "posts" + self.inheritance_column = nil + before_destroy -> { throw :abort } + end + + def test_destroy_does_not_raise_when_association_errors_on_destroy + assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do + author = AuthorWithErrorDestroyingAssociation.first + + assert_not author.destroy + end + end + + def test_destroy_with_bang_bubbles_errors_from_associations + error = assert_raises ActiveRecord::RecordNotDestroyed do + AuthorWithErrorDestroyingAssociation.first.destroy! + end + + assert_instance_of PostWithErrorDestroying, error.record + end + + def test_ids_reader_memoization + car = Car.create!(name: 'Tofaş') + bulb = Bulb.create!(car: car) + + assert_equal [bulb.id], car.bulb_ids + assert_no_queries { car.bulb_ids } + 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 1d545af5a5..b22ce42696 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -188,7 +188,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.people.include?(person) end - assert post.reload.people(true).include?(person) + assert post.reload.people.reload.include?(person) end def test_delete_all_for_with_dependent_option_destroy @@ -229,7 +229,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:thinking) post.people.concat [person] assert_equal 1, post.people.size - assert_equal 1, post.people(true).size + assert_equal 1, post.people.reload.size end def test_associate_existing_record_twice_should_add_to_target_twice @@ -285,7 +285,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.include?(new_person) end - assert posts(:thinking).reload.people(true).include?(new_person) + assert posts(:thinking).reload.people.reload.include?(new_person) end def test_associate_new_by_building @@ -310,8 +310,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:thinking).save end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob") - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Bob") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Ted") end def test_build_then_save_with_has_many_inverse @@ -356,7 +356,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_destroy_association @@ -367,7 +367,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_destroy_all @@ -378,7 +378,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_should_raise_exception_for_destroying_mismatching_records @@ -539,7 +539,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_replace_association - assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} + assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload} # 1 query to delete the existing reader (michael) # 1 query to associate the new reader (david) @@ -552,8 +552,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert !posts(:welcome).people.include?(people(:michael)) } - assert posts(:welcome).reload.people(true).include?(people(:david)) - assert !posts(:welcome).reload.people(true).include?(people(:michael)) + assert posts(:welcome).reload.people.reload.include?(people(:david)) + assert !posts(:welcome).reload.people.reload.include?(people(:michael)) end def test_replace_order_is_preserved @@ -592,7 +592,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.collect(&:first_name).include?("Jeb") end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Jeb") end def test_through_record_is_built_when_created_with_where @@ -668,7 +668,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_clear_associations - assert_queries(2) { posts(:welcome);posts(:welcome).people(true) } + assert_queries(2) { posts(:welcome);posts(:welcome).people.reload } assert_queries(1) do posts(:welcome).people.clear @@ -678,7 +678,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_association_callback_ordering @@ -750,7 +750,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations person = people(:michael) - person.posts(true) + person.posts.reload assert_queries(0) do person.post_ids person.post_ids @@ -828,14 +828,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.build(:name => "Primary") author.save assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_create_with_nonstandard_primary_key_on_belongs_to author = authors(:mary) category = author.named_categories.create(:name => "Primary") assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_exists @@ -850,7 +850,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.create(:name => "Primary") author.named_categories.delete(category) assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).empty? + assert author.named_categories.reload.empty? end def test_collection_singular_ids_getter_with_string_primary_keys @@ -871,10 +871,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do book = books(:awdr) book.subscriber_ids = [subscribers(:second).nick] - assert_equal [subscribers(:second)], book.subscribers(true) + assert_equal [subscribers(:second)], book.subscribers.reload book.subscriber_ids = [] - assert_equal [], book.subscribers(true) + assert_equal [], book.subscribers.reload end end @@ -1158,4 +1158,45 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post_through = organization.posts.build assert_equal post_direct.author_id, post_through.author_id end + + def test_has_many_through_with_scope_that_should_not_be_fully_merged + Club.has_many :distinct_memberships, -> { distinct }, class_name: "Membership" + Club.has_many :special_favourites, through: :distinct_memberships, source: :member + + 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! + TenantMembership.create!( + member: member, + club: club + ) + + TenantMembership.current_member = member + + tenant_clubs = member.tenant_clubs + assert_equal [club], tenant_clubs + + TenantMembership.current_member = nil + + other_member = Member.create! + other_club = Club.create! + TenantMembership.create!( + member: other_member, + club: other_club + ) + + tenant_clubs = other_member.tenant_clubs + assert_equal [other_club], tenant_clubs + ensure + TenantMembership.current_member = nil + 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 5c2e5e7b43..5a8afaf4d2 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -178,6 +178,25 @@ 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) @@ -332,7 +351,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert a.persisted? assert_equal a, firm.account assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_save_still_works_after_accessing_nil_has_one @@ -607,4 +627,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end end + + def test_association_force_reload_with_only_true_is_deprecated + firm = Firm.find(1) + + assert_deprecated { firm.account(true) } + 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 f8772547a2..b2b46812b9 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -16,6 +16,10 @@ require 'models/owner' require 'models/post' require 'models/comment' require 'models/categorization' +require 'models/customer' +require 'models/carrier' +require 'models/shop_account' +require 'models/customer_carrier' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -245,12 +249,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil @member_detail.member_type @member_detail.destroy assert_queries(1) do - assert_not_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_not_nil @member_detail.member_type end @member_detail.member.destroy assert_queries(1) do - assert_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_nil @member_detail.member_type end end @@ -344,4 +350,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end end + + def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + customer = Customer.create! + carrier = Carrier.create! + customer_carrier = CustomerCarrier.create!( + customer: customer, + carrier: carrier, + ) + account = ShopAccount.create!(customer_carrier: customer_carrier) + + CustomerCarrier.current_customer = customer + + account_carrier = account.carrier + assert_equal carrier, account_carrier + + CustomerCarrier.current_customer = nil + + other_carrier = Carrier.create! + other_customer = Customer.create! + other_customer_carrier = CustomerCarrier.create!( + customer: other_customer, + carrier: other_carrier, + ) + other_account = ShopAccount.create!(customer_carrier: other_customer_carrier) + + account_carrier = other_account.carrier + assert_equal other_carrier, account_carrier + ensure + CustomerCarrier.current_customer = nil + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 5575419c35..f6dddaf5b4 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -213,7 +213,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count-1, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_delete_polymorphic_has_one_with_nullify @@ -224,7 +225,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_has_many_with_piggyback @@ -461,7 +463,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert saved_post.reload.tags(true).include?(new_tag) + assert saved_post.reload.tags.reload.include?(new_tag) new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.") @@ -474,7 +476,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert new_post.reload.tags(true).include?(saved_tag) + assert new_post.reload.tags.reload.include?(saved_tag) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -490,7 +492,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase 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(true).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 }, @@ -498,7 +500,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase 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(true).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 }, @@ -506,7 +508,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase 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(true).size) + assert_equal(count + 4, post_thinking.tags.reload.size) # Raises if the wrong reflection name is used to set the Edge belongs_to assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) } @@ -544,11 +546,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase book = Book.create!(:name => 'Getting Real') book_awdr = books(:awdr) book_awdr.references << book - assert_equal(count + 1, book_awdr.references(true).size) + assert_equal(count + 1, book_awdr.references.reload.size) assert_nothing_raised { book_awdr.references.delete(book) } assert_equal(count, book_awdr.references.size) - assert_equal(count, book_awdr.references(true).size) + assert_equal(count, book_awdr.references.reload.size) assert_equal(references_before.sort, book_awdr.references.sort) end @@ -558,14 +560,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag - assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_equal(count + 1, post_thinking.taggings.reload.size) + assert_equal(count + 1, post_thinking.reload.tags.reload.size) assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) - assert_equal(count, post_thinking.taggings(true).size) + assert_equal(count, post_thinking.tags.reload.size) + assert_equal(count, post_thinking.taggings.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end @@ -577,11 +579,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.reload.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags.reload.size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) + assert_equal(count, post_thinking.tags.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 31b68c940e..b040485d99 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase groucho = members(:groucho) founding = member_types(:founding) - assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do groucho.nested_member_type = founding end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 3d202a5527..08f790e21b 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -93,8 +93,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" - 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" + 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 end def test_using_limitable_reflections_helper @@ -110,10 +112,13 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload_is_uncached firm = Firm.create!("name" => "A New Firm, Inc") Client.create!("name" => "TheClient.com", :firm => firm) - 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 {} } + + 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 diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index eeda9335ad..264b275181 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -142,7 +142,7 @@ module ActiveRecord end if current_adapter?(:PostgreSQLAdapter) - test "arrays types can be specified" do + test "array types can be specified" do klass = Class.new(OverloadedType) do attribute :my_array, :string, limit: 50, array: true attribute :my_int_array, :integer, array: true @@ -152,7 +152,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( Type::Integer.new) - refute_equal string_array, int_array + assert_not_equal string_array, int_array assert_equal string_array, klass.type_for_attribute("my_array") assert_equal int_array, klass.type_for_attribute("my_int_array") end @@ -167,7 +167,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( Type::Integer.new) - refute_equal string_range, int_range + assert_not_equal string_range, int_range assert_equal string_range, klass.type_for_attribute("my_range") assert_equal int_range, klass.type_for_attribute("my_int_range") end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 80b5a0004d..37ec3f1106 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -67,6 +67,14 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots end + def test_cyclic_autosaves_do_not_add_multiple_validations + ship = ShipWithoutNestedAttributes.new + ship.prisoners.build + + assert_not ship.valid? + assert_equal 1, ship.errors[:name].length + end + private def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) @@ -149,7 +157,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert_equal a, firm.account assert firm.save assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_assignment_before_either_saved @@ -162,7 +171,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert firm.persisted? assert a.persisted? assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_not_resaved_when_unchanged @@ -248,7 +258,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert apple.save assert apple.persisted? assert_equal apple, client.firm - assert_equal apple, client.firm(true) + client.association(:firm).reload + assert_equal apple, client.firm end def test_assignment_before_either_saved @@ -261,7 +272,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert final_cut.persisted? assert apple.persisted? assert_equal apple, final_cut.firm - assert_equal apple, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal apple, final_cut.firm end def test_store_two_association_with_one_save @@ -456,7 +468,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save assert !new_client.persisted? - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end def test_adding_before_save @@ -481,7 +493,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 2, Client.count # Clients were saved to database. assert_equal 2, new_firm.clients_of_firm.size - assert_equal 2, new_firm.clients_of_firm(true).size + assert_equal 2, new_firm.clients_of_firm.reload.size end def test_assign_ids @@ -510,7 +522,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_before_save @@ -519,7 +531,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_build_via_block_before_save @@ -530,7 +542,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_via_block_before_save @@ -543,7 +555,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_replace_on_new_object @@ -1142,6 +1154,13 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_load_the_associated_model assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } end + + def test_mark_for_destruction_is_ignored_without_autosave_true + ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") + ship.parts.build.mark_for_destruction + + assert_not ship.valid? + end end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 31c31e4329..382adbbdc7 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- require "cases/helper" -require 'active_support/concurrency/latch' require 'models/post' require 'models/author' require 'models/topic' @@ -29,6 +28,7 @@ require 'models/bird' require 'models/car' require 'models/bulb' require 'rexml/document' +require 'concurrent/atomics' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -1506,20 +1506,20 @@ class BasicsTest < ActiveRecord::TestCase orig_handler = klass.connection_handler new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new after_handler = nil - latch1 = ActiveSupport::Concurrency::Latch.new - latch2 = ActiveSupport::Concurrency::Latch.new + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new t = Thread.new do klass.connection_handler = new_handler - latch1.release - latch2.await + latch1.count_down + latch2.wait after_handler = klass.connection_handler end - latch1.await + latch1.wait klass.connection_handler = orig_handler - latch2.release + latch2.count_down t.join assert_equal after_handler, new_handler diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 0791dde1f2..48dc8135ab 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_raise_if_select_is_set_without_id - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Post.select(:title).find_each(batch_size: 1) { |post| flunk "should not call this block" } @@ -199,7 +199,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_return_an_enumerator enum = nil - assert_queries(0) do + assert_no_queries do enum = Post.find_in_batches(:batch_size => 1) end assert_queries(4) do @@ -210,6 +210,234 @@ class EachTest < ActiveRecord::TestCase end end + def test_in_batches_should_not_execute_any_query + assert_no_queries do + assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2) + end + end + + def test_in_batches_should_yield_relation_if_block_given + assert_queries(6) do + Post.in_batches(of: 2) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_be_enumerable_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_each_record_should_yield_record_if_block_is_given + assert_queries(6) do + Post.in_batches(of: 2).each_record do |post| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_return_enumerator_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_be_ordered_by_id + ids = Post.order('id ASC').pluck(:id) + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert_equal ids[i], post.id + end + end + end + + def test_in_batches_update_all_affect_all_records + assert_queries(6 + 6) do # 6 selects, 6 updates + Post.in_batches(of: 2).update_all(title: "updated-title") + end + assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count + end + + def test_in_batches_delete_all_should_not_delete_records_in_other_batches + not_deleted_count = Post.where('id <= 2').count + Post.where('id > 2').in_batches(of: 2).delete_all + assert_equal 0, Post.where('id > 2').count + assert_equal not_deleted_count, Post.count + end + + def test_in_batches_should_not_be_loaded + Post.in_batches(of: 1) do |relation| + assert_not relation.loaded? + end + + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + + def test_in_batches_should_be_loaded + Post.in_batches(of: 1, load: true) do |relation| + assert relation.loaded? + end + end + + def test_in_batches_if_not_loaded_executes_more_queries + assert_queries(@total + 1) do + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + end + + def test_in_batches_should_return_relations + assert_queries(@total + 1) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_start_from_the_start_option + post = Post.order('id ASC').where('id >= ?', 2).first + assert_queries(2) do + relation = Post.in_batches(of: 1, begin_at: 2).first + assert_equal post, relation.first + end + end + + def test_in_batches_should_end_at_the_end_option + post = Post.order('id DESC').where('id <= ?', 5).first + assert_queries(7) do + relation = Post.in_batches(of: 1, end_at: 5, load: true).reverse_each.first + assert_equal post, relation.last + end + end + + def test_in_batches_shouldnt_execute_query_unless_needed + assert_queries(2) do + Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + + assert_queries(1) do + Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + end + + 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 + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified + not_a_post = "not a post" + def not_a_post.id + raise StandardError.new("not_a_post had #id called on it") + end + + assert_nothing_raised do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + + relation = [not_a_post] * relation.count + end + end + end + + def test_in_batches_should_not_ignore_default_scope_without_order_statements + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.in_batches do |relation| + posts.concat(relation) + end + assert_equal special_posts_ids, posts.map(&:id) + end + + def test_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.in_batches({ of: 42, begin_at: 1 }.freeze){} + end + end + + def test_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.in_batches(of: 1, begin_at: start_nick) do |relation| + subscribers.concat(relation) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + assert_queries(Subscriber.count + 1) do + Subscriber.in_batches(of: 1, load: true) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Subscriber, relation.first + end + end + end + + def test_in_batches_should_return_an_enumerator + enum = nil + assert_no_queries do + enum = Post.in_batches(of: 1) + end + assert_queries(4) do + enum.first(4) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_relations_should_not_overlap_with_each_other + seen_posts = [] + Post.in_batches(of: 2, load: true) do |relation| + relation.to_a.each do |post| + assert_not seen_posts.include?(post) + seen_posts << post + end + end + end + + def test_in_batches_relations_with_condition_should_not_overlap_with_each_other + seen_posts = [] + author_id = Post.first.author_id + posts_by_author = Post.where(author_id: author_id) + Post.in_batches(of: 2) do |batch| + seen_posts += batch.where(author_id: author_id) + end + + assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort + end + + def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches + Post.update_all(author_id: 0) + person = Post.last + person.update_attributes(author_id: 1) + + Post.in_batches(of: 2) do |batch| + batch.where('author_id >= 1').update_all('author_id = author_id + 1') + end + assert_equal 2, person.reload.author_id # incremented only once + end + def test_find_in_batches_start_deprecated assert_deprecated do assert_queries(@total) do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b246eae5f5..aa10817527 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -632,6 +632,27 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id) end + def test_pluck_loaded_relation + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal ['37signals', 'Summit', 'Microsoft'], companies.pluck(:name) + end + end + + def test_pluck_loaded_relation_multiple_columns + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal [[1, '37signals'], [2, 'Summit'], [3, 'Microsoft']], companies.pluck(:id, :name) + end + end + + def test_pluck_loaded_relation_sql_fragment + companies = Company.order(:name).limit(3).load + assert_queries 1 do + assert_equal ['37signals', 'Apex', 'Ex Nihilo'], companies.pluck('DISTINCT name') + end + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! @@ -650,4 +671,14 @@ class CalculationsTest < ActiveRecord::TestCase developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count end end + + def test_sum_uses_enumerable_version_when_block_is_given + block_called = false + relation = Client.all.load + + assert_no_queries do + assert_equal 0, relation.sum { block_called = true; 0 } + end + assert block_called + end end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb new file mode 100644 index 0000000000..724234d7f4 --- /dev/null +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -0,0 +1,70 @@ +require "cases/helper" +require "models/computer" +require "models/developer" +require "models/project" +require "models/topic" +require "models/post" +require "models/comment" + +module ActiveRecord + class CollectionCacheKeyTest < ActiveRecord::TestCase + 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) + end + + test "cache_key for relation" do + developers = Developer.where(name: "David") + last_developer_timestamp = developers.order(updated_at: :desc).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 "it triggers at most one query" do + 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 + 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") + + 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) + end + + test "cache_key with custom timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:nsec) + assert_match(last_topic_timestamp, topics.cache_key(:written_on)) + end + + test "cache_key with unknown timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) } + end + + test "collection proxy provides a cache_key" do + developers = projects(:active_record).developers + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + 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 05c57985a1..7566863653 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -81,7 +81,11 @@ module ActiveRecord def test_bigint_limit cast_type = @connection.type_map.lookup("bigint") - assert_equal 8, cast_type.limit + if current_adapter?(:OracleAdapter) + assert_equal 19, cast_type.limit + else + assert_equal 8, cast_type.limit + end end def test_decimal_without_scale diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index c905772193..7ef5c93a48 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'active_support/concurrency/latch' +require 'concurrent/atomics' module ActiveRecord module ConnectionAdapters @@ -133,15 +133,15 @@ module ActiveRecord end def test_reap_inactive - ready = ActiveSupport::Concurrency::Latch.new + ready = Concurrent::CountDownLatch.new @pool.checkout child = Thread.new do @pool.checkout @pool.checkout - ready.release + ready.count_down Thread.stop end - ready.await + ready.wait assert_equal 3, active_connections(@pool).size @@ -360,13 +360,13 @@ module ActiveRecord def test_concurrent_connection_establishment assert_operator @pool.connections.size, :<=, 1 - all_threads_in_new_connection = ActiveSupport::Concurrency::Latch.new(@pool.size - @pool.connections.size) - all_go = ActiveSupport::Concurrency::Latch.new + all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size) + all_go = Concurrent::CountDownLatch.new @pool.singleton_class.class_eval do define_method(:new_connection) do - all_threads_in_new_connection.release - all_go.await + all_threads_in_new_connection.count_down + all_go.wait super() end end @@ -381,14 +381,14 @@ module ActiveRecord # the kernel of the whole test is here, everything else is just scaffolding, # this latch will not be released unless conn. pool allows for concurrent # connection creation - all_threads_in_new_connection.await + all_threads_in_new_connection.wait end rescue Timeout::Error 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 - all_go.release + all_go.count_down connecting_threads.map(&:join) end end @@ -441,11 +441,11 @@ 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 = ActiveSupport::Concurrency::Latch.new + 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.await } + pool.with_connection { second_thread_done.wait } end # wait for first_thread to get in queue @@ -456,7 +456,7 @@ module ActiveRecord # first_thread when a connection is made available second_thread = Thread.new do pool.send(group_action_method) - second_thread_done.release + second_thread_done.count_down end # wait for second_thread to get in queue @@ -471,7 +471,7 @@ module ActiveRecord failed = true unless second_thread.join(2) #--- post test clean up start - second_thread_done.release if failed + 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 diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 1f5055b2a2..922cb59280 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/car' +require 'models/aircraft' require 'models/wheel' require 'models/engine' require 'models/reply' @@ -198,4 +199,16 @@ class CounterCacheTest < ActiveRecord::TestCase assert_equal 2, car.engines_count assert_equal 2, car.reload.engines_count end + + test "update counters in a polymorphic relationship" do + aircraft = Aircraft.create! + + assert_difference 'aircraft.reload.wheels_count' do + aircraft.wheels << Wheel.create! + end + + assert_difference 'aircraft.reload.wheels_count', -1 do + aircraft.wheels.first.destroy + end + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 216f228142..f5aaf22e13 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -703,6 +703,22 @@ class DirtyTest < ActiveRecord::TestCase assert pirate.catchphrase_changed?(from: "arrrr", to: "arrrr matey!") end + test "getters with side effects are allowed" do + klass = Class.new(Pirate) do + def catchphrase + if super.blank? + update_attribute(:catchphrase, "arr") # what could possibly go wrong? + end + super + end + end + + pirate = klass.create!(catchphrase: "lol") + pirate.update_attribute(:catchphrase, nil) + + assert_equal "arr", pirate.catchphrase + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 3641826daa..7c930de97b 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -14,64 +14,78 @@ class EnumTest < ActiveRecord::TestCase assert_not @book.proposed? assert @book.read? + assert @book.in_english? + assert @book.author_visibility_visible? + assert @book.illustrator_visibility_visible? + assert @book.with_medium_font_size? end test "query state with strings" do assert_equal "published", @book.status assert_equal "read", @book.read_status + assert_equal "english", @book.language + assert_equal "visible", @book.author_visibility + assert_equal "visible", @book.illustrator_visibility end test "find via scope" do assert_equal @book, Book.published.first assert_equal @book, Book.read.first + assert_equal @book, Book.in_english.first + assert_equal @book, Book.author_visibility_visible.first + assert_equal @book, Book.illustrator_visibility_visible.first end test "find via where with values" do published, written = Book.statuses[:published], Book.statuses[:written] assert_equal @book, Book.where(status: published).first - refute_equal @book, Book.where(status: written).first + assert_not_equal @book, Book.where(status: written).first assert_equal @book, Book.where(status: [published]).first - refute_equal @book, Book.where(status: [written]).first - refute_equal @book, Book.where("status <> ?", published).first + assert_not_equal @book, Book.where(status: [written]).first + assert_not_equal @book, Book.where("status <> ?", published).first assert_equal @book, Book.where("status <> ?", written).first end test "find via where with symbols" do assert_equal @book, Book.where(status: :published).first - refute_equal @book, Book.where(status: :written).first + assert_not_equal @book, Book.where(status: :written).first assert_equal @book, Book.where(status: [:published]).first - refute_equal @book, Book.where(status: [:written]).first - refute_equal @book, Book.where.not(status: :published).first + assert_not_equal @book, Book.where(status: [:written]).first + assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end test "find via where with strings" do assert_equal @book, Book.where(status: "published").first - refute_equal @book, Book.where(status: "written").first + assert_not_equal @book, Book.where(status: "written").first assert_equal @book, Book.where(status: ["published"]).first - refute_equal @book, Book.where(status: ["written"]).first - refute_equal @book, Book.where.not(status: "published").first + assert_not_equal @book, Book.where(status: ["written"]).first + assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first end test "build from scope" do assert Book.written.build.written? - refute Book.written.build.proposed? + assert_not Book.written.build.proposed? end test "build from where" do assert Book.where(status: Book.statuses[:written]).build.written? - refute Book.where(status: Book.statuses[:written]).build.proposed? + assert_not Book.where(status: Book.statuses[:written]).build.proposed? assert Book.where(status: :written).build.written? - refute Book.where(status: :written).build.proposed? + assert_not Book.where(status: :written).build.proposed? assert Book.where(status: "written").build.written? - refute Book.where(status: "written").build.proposed? + assert_not Book.where(status: "written").build.proposed? end test "update by declaration" do @book.written! assert @book.written? + @book.in_english! + assert @book.in_english? + @book.author_visibility_visible! + assert @book.author_visibility_visible? end test "update by setter" do @@ -96,42 +110,61 @@ class EnumTest < ActiveRecord::TestCase test "enum changed attributes" do old_status = @book.status + old_language = @book.language @book.status = :proposed + @book.language = :spanish assert_equal old_status, @book.changed_attributes[:status] + assert_equal old_language, @book.changed_attributes[:language] end test "enum changes" do old_status = @book.status + old_language = @book.language @book.status = :proposed + @book.language = :spanish assert_equal [old_status, 'proposed'], @book.changes[:status] + assert_equal [old_language, 'spanish'], @book.changes[:language] end test "enum attribute was" do old_status = @book.status + old_language = @book.language @book.status = :published + @book.language = :spanish assert_equal old_status, @book.attribute_was(:status) + assert_equal old_language, @book.attribute_was(:language) end test "enum attribute changed" do @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status) + assert @book.attribute_changed?(:language) end test "enum attribute changed to" do @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status, to: 'proposed') + assert @book.attribute_changed?(:language, to: 'french') end test "enum attribute changed from" do old_status = @book.status + old_language = @book.language @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status, from: old_status) + assert @book.attribute_changed?(:language, from: old_language) end test "enum attribute changed from old status to new status" do old_status = @book.status + old_language = @book.language @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status, from: old_status, to: 'proposed') + assert @book.attribute_changed?(:language, from: old_language, to: 'french') end test "enum didn't change" do @@ -201,11 +234,15 @@ class EnumTest < ActiveRecord::TestCase test "building new objects with enum scopes" do assert Book.written.build.written? assert Book.read.build.read? + assert Book.in_spanish.build.in_spanish? + assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible? end test "creating new objects with enum scopes" do assert Book.written.create.written? assert Book.read.create.read? + assert Book.in_spanish.create.in_spanish? + assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? end test "_before_type_cast returns the enum label (required for form fields)" do @@ -355,4 +392,23 @@ class EnumTest < ActiveRecord::TestCase book2 = klass.single.create! assert book2.single? end + + test "query state by predicate with prefix" do + assert @book.author_visibility_visible? + assert_not @book.author_visibility_invisible? + assert @book.illustrator_visibility_visible? + assert_not @book.illustrator_visibility_invisible? + end + + test "query state by predicate with custom prefix" do + assert @book.in_english? + assert_not @book.in_spanish? + assert_not @book.in_french? + end + + test "uses default status when no status is provided in fixtures" do + book = books(:tlg) + assert book.proposed?, "expected fixture to default to proposed status" + assert book.in_english?, "expected fixture to default to english language" + end end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 8de2ddb10d..2dee8a26a5 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -48,6 +48,11 @@ if ActiveRecord::Base.connection.supports_explain? assert queries.empty? end + def test_collects_cte_queries + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'with s as (values(3)) select 1 from s') + assert_equal 1, queries.size + end + teardown do ActiveRecord::ExplainRegistry.reset end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 47532c84e8..76ea950fb1 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -16,6 +16,7 @@ require 'models/joke' require 'models/matey' require 'models/parrot' require 'models/pirate' +require 'models/doubloon' require 'models/post' require 'models/randomly_named_c1' require 'models/reply' @@ -216,6 +217,13 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_yaml_file_with_invalid_column + e = assert_raise(ActiveRecord::Fixture::FixtureError) do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") + end + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") @@ -903,3 +911,12 @@ class FixturesWithDefaultScopeTest < ActiveRecord::TestCase assert_equal "special", bulbs(:special).name end end + +class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase + fixtures :pirates, :doubloons + + test "creates fixtures with belongs_to associations defined in abstract base classes" do + assert_not_nil doubloons(:blackbeards_doubloon) + assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 12c793c408..d9d0f929db 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -47,7 +47,8 @@ end def mysql_56? current_adapter?(:MysqlAdapter, :Mysql2Adapter) && - ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" + ActiveRecord::Base.connection.send(:version) >= '5.6.0' && + ActiveRecord::Base.connection.send(:version) < '5.7.0' end def mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 8144f3e5c5..99230aa3d5 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -144,13 +144,17 @@ module ActiveRecord end def test_exception_on_removing_index_without_column_option - RemoveIndexMigration1.new.migrate(:up) - migration = RemoveIndexMigration2.new - migration.migrate(:up) + index_definition = ["horses", [:name, :color]] + migration1 = RemoveIndexMigration1.new + migration1.migrate(:up) + assert migration1.connection.index_exists?(*index_definition) - assert_raises(IrreversibleMigration) do - migration.migrate(:down) - end + migration2 = RemoveIndexMigration2.new + migration2.migrate(:up) + assert_not migration2.connection.index_exists?(*index_definition) + + migration2.migrate(:down) + assert migration2.connection.index_exists?(*index_definition) end def test_migrate_up diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index dbdcc84b7d..2e1363334d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -270,7 +270,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase car.wheels << Wheel.create! end assert_difference 'car.wheels.count', -1 do - car.destroy + car.reload.destroy end assert car.destroyed? end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 46a62c272f..83e50048ec 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -105,7 +105,7 @@ module ActiveRecord eight = columns.detect { |c| c.name == "eight_int" } if current_adapter?(:OracleAdapter) - assert_equal 'NUMBER(8)', eight.sql_type + assert_equal 'NUMBER(19)', eight.sql_type elsif current_adapter?(:SQLite3Adapter) assert_equal 'bigint', eight.sql_type else diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 5fc7702dfa..ab3f584350 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -267,6 +267,13 @@ module ActiveRecord assert_nil TestModel.new.first_name end + def test_change_column_default_with_from_and_to + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", from: nil, to: "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + def test_remove_column_no_second_parameter_raises_exception assert_raise(ArgumentError) { connection.remove_column("funny") } end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3844b1a92e..99f1dc65b0 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -169,6 +169,16 @@ module ActiveRecord end end + def test_invert_change_column_default_with_from_and_to + change = @recorder.inverse_of :change_column_default, [:table, :column, from: "old_value", to: "new_value"] + assert_equal [:change_column_default, [:table, :column, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_column_default_with_from_and_to_with_boolean + change = @recorder.inverse_of :change_column_default, [:table, :column, from: true, to: false] + assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change + end + def test_invert_change_column_null add = @recorder.inverse_of :change_column_null, [:table, :column, true] assert_equal [:change_column_null, [:table, :column, false]], add @@ -206,6 +216,11 @@ module ActiveRecord end def test_invert_remove_index + add = @recorder.inverse_of :remove_index, [:table, :one] + assert_equal [:add_index, [:table, :one]], add + end + + def test_invert_remove_index_with_column add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}] assert_equal [:add_index, [:table, [:one, :two], options: true]], add end @@ -281,17 +296,42 @@ module ActiveRecord assert_equal [:remove_foreign_key, [:dogs, :people]], enable end + def test_invert_remove_foreign_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + end + def test_invert_add_foreign_key_with_column enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable end + def test_invert_remove_foreign_key_with_column + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id"]], enable + end + def test_invert_add_foreign_key_with_column_and_name enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable end - def test_remove_foreign_key_is_irreversible + def test_invert_remove_foreign_key_with_column_and_name + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]], enable + end + + def test_invert_remove_foreign_key_with_primary_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, primary_key: "person_id"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable + end + + def test_invert_remove_foreign_key_with_on_delete_on_update + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] + assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable + end + + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] end @@ -299,6 +339,10 @@ module ActiveRecord assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs] + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 7f4790bf3e..72f2fa95f1 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -243,6 +243,37 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration + def change + create_table(:schools) + + create_table(:classes) do |t| + t.column :school_id, :integer + end + add_foreign_key :classes, :schools + end + end + + def test_add_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_classes").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_add_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = '_s' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("classes_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + end end end diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb new file mode 100644 index 0000000000..e4772905bb --- /dev/null +++ b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + if current_adapter?(:PostgreSQLAdapter) + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + end + + private + def assert_column_exists(column_name) + columns = connection.columns(table_name) + assert columns.map(&:name).include?(column_name.to_s) + end + + def assert_type_correct(column_name, type) + columns = connection.columns(table_name) + column = columns.select{ |c| c.name == column_name.to_s }.first + assert_equal type.to_s, column.sql_type + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 6b4addd52f..b8a0401fe3 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -658,6 +658,16 @@ module NestedAttributesOnACollectionAssociationTests assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end + def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given + other_pirate = Pirate.create! catchphrase: 'Ahoy!' + other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant' + + exception = assert_raise ActiveRecord::RecordNotFound do + @pirate.attributes = { association_getter => [{ id: other_child.id }] } + end + assert_equal "Couldn't find #{@child_1.class.name} with ID=#{other_child.id} for Pirate with ID=#{@pirate.id}", exception.message + end + def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { @@ -1054,4 +1064,39 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR assert_not part.valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end + + class ProtectedParameters + def initialize(hash) + @hash = hash + end + + def permitted? + true + end + + def [](key) + @hash[key] + end + + def to_h + @hash + end + end + + test "strong params style objects can be assigned for singular associations" do + params = { name: "Stern", ship_attributes: + ProtectedParameters.new(name: "The Black Rock") } + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + test "strong params style objects can be assigned for collection associations" do + params = { trinkets_attributes: ProtectedParameters.new("0" => ProtectedParameters.new(name: "Necklace"), "1" => ProtectedParameters.new(name: "Spoon")) } + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index b8433f0bba..0745a37ee9 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -182,6 +182,12 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function assert column.serial? end + + def test_serial_with_unquoted_sequence_name + column = Topic.columns_hash[Topic.primary_key] + assert_equal "nextval('topics_id_seq'::regclass)", column.default_function + assert column.serial? + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 9353be1ba7..37d3965022 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -242,6 +242,19 @@ module ActiveRecord assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" end + def test_select_quotes_when_using_from_clause + if sqlite3_version_includes_quoting_bug? + skip <<-ERROR.squish + You are using an outdated version of SQLite3 which has a bug in + quoted column names. Please update SQLite3 and rebuild the sqlite3 + ruby gem + ERROR + end + quoted_join = ActiveRecord::Base.connection.quote_table_name("join") + selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join) + assert_equal Post.pluck(:id), selected + end + def test_relation_merging_with_merged_joins_as_strings 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 @@ -276,5 +289,16 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + + private + + def sqlite3_version_includes_quoting_bug? + if current_adapter?(:SQLite3Adapter) + selected_quoted_column_names = ActiveRecord::Base.connection.exec_query( + 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery' + ).columns + ["join"] != selected_quoted_column_names + end + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index acbf85d398..5f48c2b40f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -647,6 +647,25 @@ class RelationTest < ActiveRecord::TestCase end end + def test_preloading_with_associations_default_scopes_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + + post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + + post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu') + result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 0d16a3526f..431fbf1297 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -3,7 +3,7 @@ require 'models/owner' require 'models/pet' class ReloadModelsTest < ActiveRecord::TestCase - fixtures :pets + fixtures :pets, :owners def test_has_one_with_reload pet = Pet.find_by_name('parrot') diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index e6f0fe6f75..feb1c29656 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -239,7 +239,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_decimal_options output = dump_all_table_schema([/^[^n]/]) - assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2\.78}, output + assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: "2\.78"}, output end if current_adapter?(:PostgreSQLAdapter) @@ -253,6 +253,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output end + def test_schema_dump_allows_array_of_decimal_defaults + output = standard_dump + assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output + end + if ActiveRecord::Base.connection.supports_extensions? def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0dbc60940e..86316ab476 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/developer' require 'models/computer' +require 'models/vehicle' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -453,4 +454,9 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, scope.where_clause.ast.children.length assert_equal Developer.where(name: "David"), scope end + + def test_with_abstract_class_where_clause_should_not_be_duplicated + scope = Bus.all + assert_equal scope.where_clause.ast.children.length, 1 + end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 7c92453ee3..6056156698 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -274,4 +274,25 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal({}, topic.content) end + + def test_values_cast_from_nil_are_persisted_as_nil + # This is required to fulfil the following contract, which must be universally + # true in Active Record: + # + # model.attribute = value + # assert_equal model.attribute, model.tap(&:save).reload.attribute + Topic.serialize(:content, Hash) + topic = Topic.create!(content: {}) + topic2 = Topic.create!(content: nil) + + assert_equal [topic, topic2], Topic.where(content: nil) + end + + def test_nil_is_always_persisted_as_null + Topic.serialize(:content, Hash) + + topic = Topic.create!(content: { foo: "bar" }) + topic.update_attribute :content, nil + assert_equal [topic], Topic.where(content: nil) + end end diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 1c449d42fe..72c5c16555 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -3,7 +3,38 @@ require 'models/notification' require 'models/user' class SuppressorTest < ActiveRecord::TestCase - def test_suppresses_creation_of_record_generated_by_callback + def test_suppresses_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + Notification.create + Notification.create! + Notification.new.save + Notification.new.save! + end + end + end + + def test_suppresses_update + user = User.create! token: 'asdf' + + User.suppress do + user.update token: 'ghjkl' + assert_equal 'asdf', user.reload.token + + user.update! token: 'zxcvbnm' + assert_equal 'asdf', user.reload.token + + user.token = 'qwerty' + user.save + assert_equal 'asdf', user.reload.token + + user.token = 'uiop' + user.save! + assert_equal 'asdf', user.reload.token + end + end + + def test_suppresses_create_in_callback assert_difference -> { User.count } do assert_no_difference -> { Notification.count } do Notification.suppress { UserWithNotification.create! } diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 084302cde5..184ff7fc63 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -204,7 +204,7 @@ module ActiveRecord end def test_structure_dump - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -212,7 +212,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -220,7 +220,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) with_dump_schemas(:all) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -228,7 +228,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -261,14 +261,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index a5e6738f14..7761ea5612 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -65,6 +65,30 @@ module ActiveRecord end end + class PostgreSQLTestCase < TestCase + def self.run(*args) + super if current_adapter?(:PostgreSQLAdapter) + end + end + + class Mysql2TestCase < TestCase + def self.run(*args) + super if current_adapter?(:Mysql2Adapter) + end + end + + class MysqlTestCase < TestCase + def self.run(*args) + super if current_adapter?(:MysqlAdapter) + end + end + + class SQLite3TestCase < TestCase + def self.run(*args) + super if current_adapter?(:SQLite3Adapter) + end + end + class SQLCounter class << self attr_accessor :ignored_sql, :log, :log_all diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 11804ff90b..b3c42c8e42 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -2,13 +2,16 @@ require 'cases/helper' require 'models/invoice' require 'models/line_item' require 'models/topic' +require 'models/node' +require 'models/tree' class TouchLaterTest < ActiveRecord::TestCase + fixtures :nodes, :trees def test_touch_laster_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do - refute invoice.persisted? + assert_not invoice.persisted? assert_raises(ActiveRecord::ActiveRecordError) do invoice.touch_later end @@ -18,7 +21,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_set_dirty_attributes invoice = Invoice.create! invoice.touch_later - refute invoice.changed? + assert_not invoice.changed? end def test_touch_later_update_the_attributes @@ -90,4 +93,22 @@ class TouchLaterTest < ActiveRecord::TestCase invoice.touch_later end end + + def test_touching_three_deep + skip "Pending from #19324" + + previous_tree_updated_at = trees(:root).updated_at + previous_grandparent_updated_at = nodes(:grandparent).updated_at + previous_parent_updated_at = nodes(:parent_a).updated_at + previous_child_updated_at = nodes(:child_one_of_a).updated_at + + travel 5.seconds + + Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + + assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at + assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at + assert_not_equal nodes(:grandparent).reload.updated_at, previous_grandparent_updated_at + assert_not_equal trees(:root).reload.updated_at, previous_tree_updated_at + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 2468a91969..29a6ec7522 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -175,13 +175,20 @@ class TransactionTest < ActiveRecord::TestCase assert topic.new_record?, "#{topic.inspect} should be new record" end + def test_transaction_state_is_cleared_when_record_is_persisted + author = Author.create! name: 'foo' + author.name = nil + assert_not author.save + assert_not author.new_record? + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) assert !status - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_update_should_rollback_on_failure! @@ -191,7 +198,7 @@ class TransactionTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) do author.update!(name: nil, post_ids: []) end - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_cancellation_from_returning_false_in_before_filter diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb new file mode 100644 index 0000000000..dd43ee358c --- /dev/null +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -0,0 +1,75 @@ +require "cases/helper" +require 'models/face' +require 'models/interest' +require 'models/man' +require 'models/topic' + +class AbsenceValidationTest < ActiveRecord::TestCase + def test_non_association + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :name + end + + assert boy_klass.new.valid? + assert_not boy_klass.new(name: "Alex").valid? + end + + def test_has_one_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + boy = boy_klass.new(face: Face.new) + assert_not boy.valid?, "should not be valid if has_one association is present" + assert_equal 1, boy.errors[:face].size, "should only add one error" + + boy.face.mark_for_destruction + assert boy.valid?, "should be valid if association is marked for destruction" + end + + def test_has_many_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :interests + end + boy = boy_klass.new + boy.interests << [i1 = Interest.new, i2 = Interest.new] + assert_not boy.valid?, "should not be valid if has_many association is present" + + i1.mark_for_destruction + assert_not boy.valid?, "should not be valid if has_many association is present" + + i2.mark_for_destruction + assert boy.valid? + end + + def test_does_not_call_to_a_on_associations + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + face_with_to_a = Face.new + def face_with_to_a.to_a; ['(/)', '(\)']; end + + assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? } + end + + def test_does_not_validate_if_parent_record_is_validate_false + repair_validations(Interest) do + Interest.validates_absence_of(:topic) + interest = Interest.new(topic: Topic.new(title: "Math")) + interest.save!(validate: false) + assert interest.persisted? + + man = Man.new(interest_ids: [interest.id]) + man.save! + + assert_equal man.interests.size, 1 + assert interest.valid? + assert man.valid? + end + end +end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 2608c84be2..ceca2c8366 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -427,4 +427,23 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert reply.valid? assert topic.valid? end + + def test_validate_uniqueness_of_custom_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "keyboards" + self.primary_key = :key_number + + validates_uniqueness_of :key_number + + def self.name + "Keyboard" + end + end + + klass.create!(key_number: 10) + key2 = klass.create!(key_number: 11) + + key2.key_number = 10 + assert_not key2.valid? + end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index 380dd3dfca..a304fba399 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -5,6 +5,10 @@ awdr: format: "paperback" status: :published read_status: :read + language: :english + author_visibility: :visible + illustrator_visibility: :visible + font_size: :medium rfr: author_id: 1 @@ -20,3 +24,8 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + +tlg: + author_id: 1 + id: 4 + name: "Thoughtleadering" diff --git a/activerecord/test/fixtures/doubloons.yml b/activerecord/test/fixtures/doubloons.yml new file mode 100644 index 0000000000..efd1643971 --- /dev/null +++ b/activerecord/test/fixtures/doubloons.yml @@ -0,0 +1,3 @@ +blackbeards_doubloon: + pirate: blackbeard + weight: 2 diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml new file mode 100644 index 0000000000..3e10331105 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -0,0 +1,2 @@ +george: + arrr: "Curious George" diff --git a/activerecord/test/fixtures/nodes.yml b/activerecord/test/fixtures/nodes.yml new file mode 100644 index 0000000000..b8bb8216ee --- /dev/null +++ b/activerecord/test/fixtures/nodes.yml @@ -0,0 +1,29 @@ +grandparent: + id: 1 + tree_id: 1 + name: Grand Parent + +parent_a: + id: 2 + tree_id: 1 + parent_id: 1 + name: Parent A + +parent_b: + id: 3 + tree_id: 1 + parent_id: 1 + name: Parent B + +child_one_of_a: + id: 4 + tree_id: 1 + parent_id: 2 + name: Child one + +child_two_of_b: + id: 5 + tree_id: 1 + parent_id: 2 + name: Child two + diff --git a/activerecord/test/fixtures/trees.yml b/activerecord/test/fixtures/trees.yml new file mode 100644 index 0000000000..9e030b7632 --- /dev/null +++ b/activerecord/test/fixtures/trees.yml @@ -0,0 +1,3 @@ +root: + id: 1 + name: The Root diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb index 1f35ef45da..c4404a8094 100644 --- a/activerecord/test/models/aircraft.rb +++ b/activerecord/test/models/aircraft.rb @@ -1,4 +1,5 @@ class Aircraft < ActiveRecord::Base self.pluralize_table_names = false has_many :engines, :foreign_key => "car_id" + has_many :wheels, as: :wheelable end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 2170018068..1927191393 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -10,6 +10,10 @@ class Book < ActiveRecord::Base enum status: [:proposed, :written, :published] enum read_status: {unread: 0, reading: 2, read: 3} enum nullable_status: [:single, :married] + enum language: [:english, :spanish, :french], _prefix: :in + enum author_visibility: [:visible, :invisible], _prefix: true + enum illustrator_visibility: [:visible, :invisible], _prefix: true + enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true def published! super diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb new file mode 100644 index 0000000000..230be118c3 --- /dev/null +++ b/activerecord/test/models/carrier.rb @@ -0,0 +1,2 @@ +class Carrier < ActiveRecord::Base +end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 6588531de6..4cd67c970a 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,6 +1,6 @@ class Categorization < ActiveRecord::Base belongs_to :post - belongs_to :category + belongs_to :category, counter_cache: true belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb new file mode 100644 index 0000000000..37186903ff --- /dev/null +++ b/activerecord/test/models/customer_carrier.rb @@ -0,0 +1,14 @@ +class CustomerCarrier < ActiveRecord::Base + cattr_accessor :current_customer + + belongs_to :customer + belongs_to :carrier + + default_scope -> { + if current_customer + where(customer: current_customer) + else + all + end + } +end diff --git a/activerecord/test/models/doubloon.rb b/activerecord/test/models/doubloon.rb new file mode 100644 index 0000000000..2b11d128e2 --- /dev/null +++ b/activerecord/test/models/doubloon.rb @@ -0,0 +1,12 @@ +class AbstractDoubloon < ActiveRecord::Base + # This has functionality that might be shared by multiple classes. + + self.abstract_class = true + belongs_to :pirate +end + +class Doubloon < AbstractDoubloon + # This uses an abstract class that defines attributes and associations. + + self.table_name = 'doubloons' +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index dc0566d8a7..7693c6e515 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -26,6 +26,9 @@ class Member < ActiveRecord::Base has_many :current_memberships, -> { where :favourite => true } has_many :clubs, :through => :current_memberships + has_many :tenant_memberships + has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club + has_one :club_through_many, :through => :current_memberships, :source => :club belongs_to :admittable, polymorphic: true diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 9d253aa126..157130986c 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -1,7 +1,8 @@ class MemberDetail < ActiveRecord::Base - belongs_to :member, :inverse_of => false + belongs_to :member, inverse_of: false belongs_to :organization - has_one :member_type, :through => :member + has_one :member_type, through: :member + has_one :membership, through: :member - has_many :organization_member_details, :through => :organization, :source => :member_details + has_many :organization_member_details, through: :organization, source: :member_details end diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index df7167ee93..e181ba1f11 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -18,3 +18,18 @@ class SelectedMembership < Membership select("'1' as foo") end end + +class TenantMembership < Membership + cattr_accessor :current_member + + belongs_to :member + belongs_to :club + + default_scope -> { + if current_member + where(member: current_member) + else + all + end + } +end diff --git a/activerecord/test/models/node.rb b/activerecord/test/models/node.rb new file mode 100644 index 0000000000..07dd2dbccb --- /dev/null +++ b/activerecord/test/models/node.rb @@ -0,0 +1,5 @@ +class Node < ActiveRecord::Base + belongs_to :tree, touch: true + belongs_to :parent, class_name: 'Node', touch: true, optional: true + has_many :children, class_name: 'Node', foreign_key: :parent_id, dependent: :destroy +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 052b1c9690..81a18188d4 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -98,11 +98,11 @@ class Post < ActiveRecord::Base end end - has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all - has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all, counter_cache: :taggings_with_delete_all_count + has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy, counter_cache: :taggings_with_destroy_count - has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy - has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify + has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy, counter_cache: :tags_with_destroy_count + has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify, counter_cache: :tags_with_nullify_count has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag has_many :funky_tags, :through => :taggings, :source => :tag @@ -208,6 +208,22 @@ class PostWithDefaultScope < ActiveRecord::Base default_scope { order(:title) } end +class PostWithPreloadDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { preload(:readers) } +end + +class PostWithIncludesDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { includes(:readers) } +end + class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb new file mode 100644 index 0000000000..7654eda0ef --- /dev/null +++ b/activerecord/test/models/professor.rb @@ -0,0 +1,5 @@ +require_dependency 'models/arunit2_model' + +class Professor < ARUnit2Model + has_and_belongs_to_many :courses +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 312caef604..95172e4d3e 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -19,6 +19,18 @@ class Ship < ActiveRecord::Base end end +class ShipWithoutNestedAttributes < ActiveRecord::Base + self.table_name = "ships" + has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id + has_many :parts, class_name: "ShipPart", foreign_key: :ship_id + + validates :name, presence: true +end + +class Prisoner < ActiveRecord::Base + belongs_to :ship, autosave: true, class_name: "ShipWithoutNestedAttributes", inverse_of: :prisoners +end + class FamousShip < ActiveRecord::Base self.table_name = 'ships' belongs_to :famous_pirate diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb new file mode 100644 index 0000000000..1580e8b20c --- /dev/null +++ b/activerecord/test/models/shop_account.rb @@ -0,0 +1,6 @@ +class ShopAccount < ActiveRecord::Base + belongs_to :customer + belongs_to :customer_carrier + + has_one :carrier, through: :customer_carrier +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index f81ffe1d90..d17270021a 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -32,7 +32,7 @@ class Topic < ActiveRecord::Base end end - has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :replies, dependent: :destroy, foreign_key: "parent_id", autosave: true has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count' has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index ffc65466d5..63ff0c23ec 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -1,6 +1,7 @@ class Treasure < ActiveRecord::Base has_and_belongs_to_many :parrots belongs_to :looter, :polymorphic => true + # No counter_cache option given belongs_to :ship has_many :price_estimates, :as => :estimate_of diff --git a/activerecord/test/models/tree.rb b/activerecord/test/models/tree.rb new file mode 100644 index 0000000000..dc29cccc9c --- /dev/null +++ b/activerecord/test/models/tree.rb @@ -0,0 +1,3 @@ +class Tree < ActiveRecord::Base + has_many :nodes, dependent: :destroy +end diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb new file mode 100644 index 0000000000..ef26170f1f --- /dev/null +++ b/activerecord/test/models/vehicle.rb @@ -0,0 +1,7 @@ +class Vehicle < ActiveRecord::Base + self.abstract_class = true + default_scope -> { where("tires_count IS NOT NULL") } +end + +class Bus < Vehicle +end
\ No newline at end of file diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 872fa595b4..df0362573b 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -107,5 +107,6 @@ _SQL create_table :bigint_array, force: true do |t| t.integer :big_int_data_points, limit: 8, array: true + t.decimal :decimal_array_default, array: true, default: [1.23, 3.45] end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 66f8f1611d..6f34115534 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,6 +37,7 @@ ActiveRecord::Schema.define do create_table :aircraft, force: true do |t| t.string :name + t.integer :wheels_count, default: 0, null: false end create_table :articles, force: true do |t| @@ -100,6 +101,10 @@ ActiveRecord::Schema.define do t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 t.column :nullable_status, :integer + t.column :language, :integer, default: 0 + t.column :author_visibility, :integer, default: 0 + t.column :illustrator_visibility, :integer, default: 0 + t.column :font_size, :integer, default: 0 end create_table :booleans, force: true do |t| @@ -126,6 +131,8 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :carriers, force: true + create_table :categories, force: true do |t| t.string :name, null: false t.string :type @@ -232,6 +239,11 @@ ActiveRecord::Schema.define do t.string :gps_location end + create_table :customer_carriers, force: true do |t| + t.references :customer + t.references :carrier + end + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name @@ -266,6 +278,11 @@ ActiveRecord::Schema.define do t.string :alias end + create_table :doubloons, force: true do |t| + t.integer :pirate_id + t.integer :weight + end + create_table :edges, force: true, id: false do |t| t.column :source_id, :integer, null: false t.column :sink_id, :integer, null: false @@ -657,6 +674,8 @@ ActiveRecord::Schema.define do t.string :name t.integer :pirate_id t.integer :update_only_pirate_id + # Conventionally named column for counter_cache + t.integer :treasures_count, default: 0 t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -669,6 +688,15 @@ ActiveRecord::Schema.define do t.datetime :updated_at end + create_table :prisoners, force: true do |t| + t.belongs_to :ship + end + + create_table :shop_accounts, force: true do |t| + t.references :customer + t.references :customer_carrier + end + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name @@ -858,6 +886,17 @@ ActiveRecord::Schema.define do t.string 'from' end + create_table :nodes, force: true do |t| + t.integer :tree_id + t.integer :parent_id + t.string :name + t.datetime :updated_at + end + create_table :trees, force: true do |t| + t.string :name + t.datetime :updated_at + end + create_table :hotels, force: true do |t| end create_table :departments, force: true do |t| @@ -914,3 +953,12 @@ end College.connection.create_table :colleges, force: true do |t| t.column :name, :string, null: false end + +Professor.connection.create_table :professors, force: true do |t| + t.column :name, :string, null: false +end + +Professor.connection.create_table :courses_professors, id: false, force: true do |t| + t.references :course + t.references :professor +end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index d11fd9cfc1..c5334e8596 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,6 +1,7 @@ require 'active_support/logger' require 'models/college' require 'models/course' +require 'models/professor' module ARTest def self.connection_name diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index e77ebcb2f2..eaaa3df061 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,40 @@ +* Deprecate `:prefix` option of `number_to_human_size` with no replacement. + + *Jean Boussier* + +* Fix `TimeWithZone#eql?` to properly handle `TimeWithZone` created from `DateTime`: + twz = DateTime.now.in_time_zone + twz.eql?(twz.dup) => true + + Fixes #14178. + + *Roque Pinel* + +* ActiveSupport::HashWithIndifferentAccess `select` and `reject` will now return + enumerator if called without block. + + Fixes #20095 + + *Bernard Potocki* + +* Removed `ActiveSupport::Concurrency::Latch`, superseded by `Concurrent::CountDownLatch` + from the concurrent-ruby gem. + + *Jerry D'Antonio* + +* Fix not calling `#default` on `HashWithIndifferentAccess#to_hash` when only + `default_proc` is set, which could raise. + + *Simon Eskildsen* + +* Fix setting `default_proc` on `HashWithIndifferentAccess#dup` + + *Simon Eskildsen* + +* Fix a range of values for parameters of the Time#change + + *Nikolay Kondratyev* + * Add `Enumerable#pluck` to get the same values from arrays as from ActiveRecord associations. @@ -10,7 +47,7 @@ Before: - if (slack_url = Rails.application.secrets.slack_url).present?) + if (slack_url = Rails.application.secrets.slack_url).present? # Do something worthwhile else # Raise as important secret password is not specified diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 12c6a4d449..6d2ae9be76 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -25,4 +25,6 @@ Gem::Specification.new do |s| s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4' + s.add_dependency 'concurrent-ruby', '~> 0.9.0' + s.add_dependency 'method_source' end diff --git a/activesupport/bin/test b/activesupport/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/activesupport/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d06f22ad5c..e161ec4cca 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -25,7 +25,7 @@ module ActiveSupport # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt> # These two methods will give you a completely untouched backtrace. # - # Inspired by the Quiet Backtrace gem by Thoughtbot. + # Inspired by the Quiet Backtrace gem by thoughtbot. class BacktraceCleaner def initialize @filters, @silencers = [], [] diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e8ab3a7db5..fefba5b0fd 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -80,8 +80,12 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - callbacks = send("_#{kind}_callbacks") + send "_run_#{kind}_callbacks", &block + end + + private + def __run_callbacks__(callbacks, &block) if callbacks.empty? yield if block_given? else @@ -91,8 +95,6 @@ module ActiveSupport end end - private - # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. @@ -739,10 +741,10 @@ module ActiveSupport # callback chain, preventing following before and around callbacks from # being called and the event from being triggered. # This should be a lambda to be executed. - # The current object and the return result of the callback will be called - # with the lambda. + # The current object and the result lambda of the callback will be provided + # to the terminator lambda. # - # define_callbacks :validate, terminator: ->(target, result) { result == false } + # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false } # # In this example, if any before validate callbacks returns +false+, # any successive before and around callback is not executed. @@ -806,6 +808,12 @@ module ActiveSupport names.each do |name| class_attribute "_#{name}_callbacks" set_callbacks name, CallbackChain.new(name, options) + + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _run_#{name}_callbacks(&block) + __run_callbacks__(_#{name}_callbacks, &block) + end + RUBY end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 4082d2d464..0403eb70ca 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -132,7 +132,7 @@ module ActiveSupport end def class_methods(&class_methods_module_definition) - mod = const_defined?(:ClassMethods) ? + mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : const_set(:ClassMethods, Module.new) diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb index 1507de433e..7b8df0df04 100644 --- a/activesupport/lib/active_support/concurrency/latch.rb +++ b/activesupport/lib/active_support/concurrency/latch.rb @@ -1,26 +1,18 @@ -require 'thread' -require 'monitor' +require 'concurrent/atomics' module ActiveSupport module Concurrency - class Latch - def initialize(count = 1) - @count = count - @lock = Monitor.new - @cv = @lock.new_cond - end + class Latch < Concurrent::CountDownLatch - def release - @lock.synchronize do - @count -= 1 if @count > 0 - @cv.broadcast if @count.zero? - end + def initialize(count = 1) + ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") + super(count) end + + alias_method :release, :count_down def await - @lock.synchronize do - @cv.wait_while { @count > 0 } - end + wait(nil) end end end diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb new file mode 100644 index 0000000000..ca48164c54 --- /dev/null +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -0,0 +1,142 @@ +require 'thread' +require 'monitor' + +module ActiveSupport + module Concurrency + # A share/exclusive lock, otherwise known as a read/write lock. + # + # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock + #-- + # Note that a pending Exclusive lock attempt does not block incoming + # Share requests (i.e., we are "read-preferring"). That seems + # consistent with the behavior of "loose" upgrades, but may be the + # wrong choice otherwise: it nominally reduces the possibility of + # deadlock by risking starvation instead. + class ShareLock + include MonitorMixin + + # We track Thread objects, instead of just using counters, because + # we need exclusive locks to be reentrant, and we need to be able + # to upgrade share locks to exclusive. + + + def initialize + super() + + @cv = new_cond + + @sharing = Hash.new(0) + @waiting = {} + @exclusive_thread = nil + @exclusive_depth = 0 + end + + # Returns false if +no_wait+ is set and the lock is not + # immediately available. Otherwise, returns true after the lock + # has been acquired. + # + # +purpose+ and +compatible+ work together; while this thread is + # waiting for the exclusive lock, it will yield its share (if any) + # to any other attempt whose +purpose+ appears in this attempt's + # +compatible+ list. This allows a "loose" upgrade, which, being + # less strict, prevents some classes of deadlocks. + # + # For many resources, loose upgrades are sufficient: if a thread + # is awaiting a lock, it is not running any other code. With + # +purpose+ matching, it is possible to yield only to other + # threads whose activity will not interfere. + def start_exclusive(purpose: nil, compatible: [], no_wait: false) + synchronize do + unless @exclusive_thread == Thread.current + if busy?(purpose) + return false if no_wait + + loose_shares = @sharing.delete(Thread.current) + @waiting[Thread.current] = compatible if loose_shares + + begin + @cv.wait_while { busy?(purpose) } + ensure + @waiting.delete Thread.current + @sharing[Thread.current] = loose_shares if loose_shares + end + end + @exclusive_thread = Thread.current + end + @exclusive_depth += 1 + + true + end + end + + # Relinquish the exclusive lock. Must only be called by the thread + # that called start_exclusive (and currently holds the lock). + def stop_exclusive + synchronize do + raise "invalid unlock" if @exclusive_thread != Thread.current + + @exclusive_depth -= 1 + if @exclusive_depth == 0 + @exclusive_thread = nil + @cv.broadcast + end + end + end + + def start_sharing + synchronize do + if @exclusive_thread && @exclusive_thread != Thread.current + @cv.wait_while { @exclusive_thread } + end + @sharing[Thread.current] += 1 + end + end + + def stop_sharing + synchronize do + if @sharing[Thread.current] > 1 + @sharing[Thread.current] -= 1 + else + @sharing.delete Thread.current + @cv.broadcast + end + end + end + + # Execute the supplied block while holding the Exclusive lock. If + # +no_wait+ is set and the lock is not immediately available, + # returns +nil+ without yielding. Otherwise, returns the result of + # the block. + # + # See +start_exclusive+ for other options. + def exclusive(purpose: nil, compatible: [], no_wait: false) + if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait) + begin + yield + ensure + stop_exclusive + end + end + end + + # Execute the supplied block while holding the Share lock. + def sharing + start_sharing + begin + yield + ensure + stop_sharing + end + end + + private + + # Must be called within synchronize + def busy?(purpose) + (@exclusive_thread && @exclusive_thread != Thread.current) || + @waiting.any? { |k, v| k != Thread.current && !v.include?(purpose) } || + @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0) + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 86d7fa1abf..c60e833441 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -1,13 +1,12 @@ require 'date' +require 'active_support/duration' require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/date/zones' -require 'active_support/core_ext/date/operators' require 'active_support/core_ext/time/zones' require 'active_support/core_ext/date_and_time/calculations' class Date include DateAndTime::Calculations - prepend ActiveSupport::DateOperators class << self attr_accessor :beginning_of_week_default @@ -86,6 +85,26 @@ class Date end alias :at_end_of_day :end_of_day + def plus_with_duration(other) #:nodoc: + if ActiveSupport::Duration === other + other.since(self) + else + plus_without_duration(other) + end + end + alias_method :plus_without_duration, :+ + alias_method :+, :plus_with_duration + + def minus_with_duration(other) #:nodoc: + if ActiveSupport::Duration === other + plus_with_duration(-other) + else + minus_without_duration(other) + end + end + alias_method :minus_without_duration, :- + alias_method :-, :minus_with_duration + # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>. def advance(options) @@ -110,4 +129,15 @@ class Date options.fetch(:day, day) ) end + + # 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 + else + compare_without_coercion(other) + end + end + alias_method :compare_without_coercion, :<=> + alias_method :<=>, :compare_with_coercion end diff --git a/activesupport/lib/active_support/core_ext/date/operators.rb b/activesupport/lib/active_support/core_ext/date/operators.rb deleted file mode 100644 index decf099624..0000000000 --- a/activesupport/lib/active_support/core_ext/date/operators.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'active_support/core_ext/date_and_time/with_duration' - -module ActiveSupport - module DateOperators # :nodoc: - include DateAndTime::WithDuration - - # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. - def <=>(other) - if other.is_a?(Time) - self.to_datetime <=> other - else - super - end - end - end -end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 01153606c9..40811dafc0 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 @@ -92,15 +92,28 @@ module DateAndTime end # Returns a new date/time at the start of the month. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Thu, 18 Jun 2015 + # today.beginning_of_month # => Mon, 01 Jun 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000 + # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000 def beginning_of_month first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month # Returns a new date/time at the start of the quarter. - # Example: 1st January, 1st July, 1st October. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_quarter # => Wed, 01 Jul 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } beginning_of_month.change(:month => first_quarter_month) @@ -108,8 +121,14 @@ module DateAndTime alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new date/time at the end of the quarter. - # Example: 31st March, 30th June, 30th September. - # DateTime objects will have a time set to 23:59:59. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.end_of_quarter # => Wed, 30 Sep 2015 + # + # +DateTime+ objects will have a time set to 23:59:59. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } beginning_of_month.change(:month => last_quarter_month).end_of_month @@ -117,8 +136,14 @@ module DateAndTime alias :at_end_of_quarter :end_of_quarter # Return a new date/time at the beginning of the year. - # Example: 1st January. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_year # => Thu, 01 Jan 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000 def beginning_of_year change(:month => 1).beginning_of_month end @@ -138,8 +163,8 @@ module DateAndTime # # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. # - # now = Time.current # => Thu, 07 May 2015 13:31:16 UTC +00:00 - # now.next_week # => Mon, 11 May 2015 00:00:00 UTC +00:00 + # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000 + # now.next_week # => Mon, 11 May 2015 00:00:00 +0000 def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) same_time ? copy_time_to(result) : result diff --git a/activesupport/lib/active_support/core_ext/date_and_time/with_duration.rb b/activesupport/lib/active_support/core_ext/date_and_time/with_duration.rb deleted file mode 100644 index 4a6c4013d5..0000000000 --- a/activesupport/lib/active_support/core_ext/date_and_time/with_duration.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'active_support/duration' - -module ActiveSupport - module DateAndTime - module WithDuration #:nodoc: - def +(other) #:nodoc: - if ActiveSupport::Duration === other - other.since(self) - else - super - end - end - - def -(other) #:nodoc: - if ActiveSupport::Duration === other - self + (-other) - else - super - end - end - 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 55ad384f4f..95617fb8c2 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -168,7 +168,7 @@ class DateTime if other.kind_of?(Infinity) super elsif other.respond_to? :to_datetime - super other.to_datetime + super other.to_datetime rescue nil else nil end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index d28f26260e..fc7531d088 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -76,8 +76,15 @@ module Enumerable # # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"] - def pluck(key) - map { |element| element[key] } + # + # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) + # => [[1, "David"], [2, "Rafael"]] + def pluck(*keys) + if keys.many? + map { |element| keys.map { |key| element[key] } } + else + map { |element| element[keys.first] } + 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 2149d4439d..8594d9bf2e 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -106,7 +106,25 @@ class Hash # # => {"hash"=>{"foo"=>1, "bar"=>2}} # # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or - # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to + # parse this XML. + # + # Custom +disallowed_types+ can also be passed in the form of an + # array. + # + # xml = <<-XML + # <?xml version="1.0" encoding="UTF-8"?> + # <hash> + # <foo type="integer">1</foo> + # <bar type="string">"David"</bar> + # </hash> + # XML + # + # hash = Hash.from_xml(xml, ['integer']) + # # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer" + # + # Note that passing custom disallowed types will override the default types, + # which are Symbol and YAML. def from_xml(xml, disallowed_types = nil) ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 6e397abf51..2f6d38c1f6 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,8 +1,9 @@ class Hash - # Returns a hash that includes everything but the given keys. - # hash = { a: true, b: false, c: nil} - # hash.except(:c) # => { a: true, b: false} - # hash # => { a: true, b: false, c: nil} + # Returns a hash that includes everything except given keys. + # hash = { a: true, b: false, c: nil } + # hash.except(:c) # => { a: true, b: false } + # hash.except(:a, :b) # => { c: nil } + # hash # => { a: true, b: false, c: nil } # # This is useful for limiting a set of parameters to everything but a few known toggles: # @person.update(params[:person].except(:admin)) @@ -10,10 +11,10 @@ class Hash dup.except!(*keys) end - # Replaces the hash without the given keys. - # hash = { a: true, b: false, c: nil} - # hash.except!(:c) # => { a: true, b: false} - # hash # => { a: true, b: false } + # Removes the given keys from hash and returns it. + # hash = { a: true, b: false, c: nil } + # hash.except!(:c) # => { a: true, b: false } + # hash # => { a: true, b: false } def except!(*keys) keys.each { |key| delete(key) } self diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index d9fb392752..60732eb41a 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -23,7 +23,7 @@ class LoadError # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) - location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') + location.sub(/\.rb$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index d4e6b5a1ac..a77da573fe 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -206,7 +206,7 @@ class Module # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) - mattr_writer(*syms, &blk) + mattr_writer(*syms) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index e26b594fc4..65b88b9bbd 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -99,7 +99,7 @@ class Module # end # # Todo.ancestors - # # => Todo, Todo::EventTracking, Object + # # => [Todo, Todo::EventTracking, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 9b7a429db9..9dc0dee1bd 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -167,7 +167,7 @@ class Module '' end - file, line = caller(1, 1).first.split(':', 2) + file, line = caller(1, 1).first.split(':'.freeze, 2) line = line.to_i to = to.to_s diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 620f7b6561..befa5aee21 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, number objects; + # False for +nil+, +false+, +true+, symbol, number, method objects; # true otherwise. def duplicable? true @@ -78,6 +78,10 @@ end require 'bigdecimal' class BigDecimal + # BigDecimals are duplicable: + # + # BigDecimal.new("1.2").duplicable? # => true + # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> def duplicable? true end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 69be6c4abc..c67eb25b68 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -8,7 +8,7 @@ module ActiveSupport def try!(*a, &b) if a.empty? && block_given? - if b.arity.zero? + if b.arity == 0 instance_eval(&b) else yield self diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 97f9720b2b..b2e713077c 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -164,7 +164,7 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> - def parameterize(sep = '-') + def parameterize(sep = '-'.freeze) ActiveSupport::Inflector.parameterize(self, sep) end @@ -172,7 +172,7 @@ class String # uses the +pluralize+ method on the last word in the string. # # 'RawScaledScorer'.tableize # => "raw_scaled_scorers" - # 'egg_and_ham'.tableize # => "egg_and_hams" + # 'ham_and_egg'.tableize # => "ham_and_eggs" # 'fancyCategory'.tableize # => "fancy_categories" def tableize ActiveSupport::Inflector.tableize(self) @@ -182,7 +182,7 @@ class String # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # - # 'egg_and_hams'.classify # => "EggAndHam" + # 'ham_and_eggs'.classify # => "HamAndEgg" # 'posts'.classify # => "Post" def classify ActiveSupport::Inflector.classify(self) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index dd99dd667a..96156deebb 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -1,12 +1,11 @@ +require 'active_support/duration' require 'active_support/core_ext/time/conversions' require 'active_support/time_with_zone' require 'active_support/core_ext/time/zones' -require 'active_support/core_ext/time/operators' require 'active_support/core_ext/date_and_time/calculations' class Time include DateAndTime::Calculations - prepend ActiveSupport::TimeOperators COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] @@ -99,7 +98,7 @@ class Time elsif zone ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) else - raise ArgumentError, 'argument out of range' if new_usec > 999999 + 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) end end @@ -109,6 +108,12 @@ class Time # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>, # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>, # <tt>:seconds</tt>. + # + # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700 def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) @@ -213,4 +218,60 @@ class Time def all_day beginning_of_day..end_of_day end + + def plus_with_duration(other) #:nodoc: + if ActiveSupport::Duration === other + other.since(self) + else + plus_without_duration(other) + end + end + alias_method :plus_without_duration, :+ + alias_method :+, :plus_with_duration + + def minus_with_duration(other) #:nodoc: + if ActiveSupport::Duration === other + other.until(self) + else + minus_without_duration(other) + end + end + alias_method :minus_without_duration, :- + alias_method :-, :minus_with_duration + + # Time#- can also be used to determine the number of seconds between two Time instances. + # We're layering on additional behavior so that ActiveSupport::TimeWithZone instances + # are coerced into values that Time#- will recognize + def minus_with_coercion(other) + other = other.comparable_time if other.respond_to?(:comparable_time) + other.is_a?(DateTime) ? to_f - other.to_f : minus_without_coercion(other) + end + alias_method :minus_without_coercion, :- + alias_method :-, :minus_with_coercion + + # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances + # can be chronologically compared with a Time + def compare_with_coercion(other) + # we're avoiding Time#to_datetime and Time#to_time because they're expensive + if other.class == Time + compare_without_coercion(other) + elsif other.is_a?(Time) + compare_without_coercion(other.to_time) + else + to_datetime <=> other + end + end + alias_method :compare_without_coercion, :<=> + alias_method :<=>, :compare_with_coercion + + # Layers additional behavior on Time#eql? so that ActiveSupport::TimeWithZone instances + # can be eql? to an equivalent Time + def eql_with_coercion(other) + # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do eql? comparison + other = other.comparable_time if other.respond_to?(:comparable_time) + eql_without_coercion(other) + end + alias_method :eql_without_coercion, :eql? + alias_method :eql?, :eql_with_coercion + end diff --git a/activesupport/lib/active_support/core_ext/time/operators.rb b/activesupport/lib/active_support/core_ext/time/operators.rb deleted file mode 100644 index 161304dcb3..0000000000 --- a/activesupport/lib/active_support/core_ext/time/operators.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'active_support/core_ext/date_and_time/with_duration' - -module ActiveSupport - module TimeOperators # :nodoc: - include DateAndTime::WithDuration - - # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances - # can be chronologically compared with a Time - def <=>(other) - # we're avoiding Time#to_datetime and Time#to_time because they're expensive - if other.class == Time - super - elsif other.is_a?(Time) - super(other.to_time) - else - to_datetime <=> other - end - end - - # Layers additional behavior on Time#eql? so that ActiveSupport::TimeWithZone instances - # can be eql? to an equivalent Time - def eql?(other) - # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do eql? comparison - other = other.comparable_time if other.respond_to?(:comparable_time) - super - end - - # Time#- can also be used to determine the number of seconds between two Time instances. - # We're layering on additional behavior so that ActiveSupport::TimeWithZone instances - # are coerced into values that Time#- will recognize - def -(other) - other = other.comparable_time if other.respond_to?(:comparable_time) - other.is_a?(DateTime) ? to_f - other.to_f : super - end - end -end diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index d683e7c777..133d3938eb 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -65,7 +65,8 @@ class Time if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone else - # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) + # Look up the timezone based on the identifier (unless we've been + # passed a TZInfo::Timezone) unless time_zone.respond_to?(:period_for_local) time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index bfe0832b37..0b2ff817c3 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -12,7 +12,7 @@ unless str == parser.unescape(parser.escape(str)) # YK: My initial experiments say yes, but let's be sure please enc = str.encoding enc = Encoding::UTF_8 if enc == Encoding::US_ASCII - str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(enc) + str.gsub(escaped) { |match| [match[1, 2].hex].pack('C') }.force_encoding(enc) end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 664cc15a29..f76ef04f49 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -12,12 +12,40 @@ require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' require 'active_support/core_ext/string/starts_ends_with' +require "active_support/dependencies/interlock" require 'active_support/inflector' module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self + mattr_accessor :interlock + self.interlock = Interlock.new + + # :doc: + + # Execute the supplied block without interference from any + # concurrent loads + def self.run_interlock + Dependencies.interlock.running { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time + def self.load_interlock + Dependencies.interlock.loading { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time + def self.unload_interlock + Dependencies.interlock.unloading { yield } + end + + # :nodoc: + # Should we turn on Ruby warnings on the first load of dependent files? mattr_accessor :warnings_on_first_load self.warnings_on_first_load = false @@ -128,7 +156,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::") + constants << ([namespace, suffix] - ["Object"]).join("::".freeze) end end constants @@ -325,9 +353,11 @@ module ActiveSupport #:nodoc: def clear log_call - loaded.clear - loading.clear - remove_unloadable_constants! + Dependencies.unload_interlock do + loaded.clear + loading.clear + remove_unloadable_constants! + end end def require_or_load(file_name, const_path = nil) @@ -336,39 +366,44 @@ module ActiveSupport #:nodoc: expanded = File.expand_path(file_name) return if loaded.include?(expanded) - # Record that we've seen this file *before* loading it to avoid an - # infinite loop with mutual dependencies. - loaded << expanded - loading << expanded + Dependencies.load_interlock do + # Maybe it got loaded while we were waiting for our lock: + return if loaded.include?(expanded) - begin - if load? - log "loading #{file_name}" + # Record that we've seen this file *before* loading it to avoid an + # infinite loop with mutual dependencies. + loaded << expanded + loading << expanded - # Enable warnings if this file has not been loaded before and - # warnings_on_first_load is set. - load_args = ["#{file_name}.rb"] - load_args << const_path unless const_path.nil? - - if !warnings_on_first_load or history.include?(expanded) - result = load_file(*load_args) + begin + if load? + log "loading #{file_name}" + + # Enable warnings if this file has not been loaded before and + # warnings_on_first_load is set. + load_args = ["#{file_name}.rb"] + load_args << const_path unless const_path.nil? + + if !warnings_on_first_load or history.include?(expanded) + result = load_file(*load_args) + else + enable_warnings { result = load_file(*load_args) } + end else - enable_warnings { result = load_file(*load_args) } + log "requiring #{file_name}" + result = require file_name end - else - log "requiring #{file_name}" - result = require file_name + rescue Exception + loaded.delete expanded + raise + ensure + loading.pop end - rescue Exception - loaded.delete expanded - raise - ensure - loading.pop - end - # Record history *after* loading so first load gets warnings. - history << expanded - result + # Record history *after* loading so first load gets warnings. + history << expanded + result + end end # Is the provided constant path defined? @@ -401,7 +436,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb") + path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -486,7 +521,7 @@ module ActiveSupport #:nodoc: if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, '') + expanded.sub!(/\.rb\z/, ''.freeze) if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb new file mode 100644 index 0000000000..fbeb904684 --- /dev/null +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -0,0 +1,47 @@ +require 'active_support/concurrency/share_lock' + +module ActiveSupport #:nodoc: + module Dependencies #:nodoc: + class Interlock + def initialize # :nodoc: + @lock = ActiveSupport::Concurrency::ShareLock.new + end + + def loading + @lock.exclusive(purpose: :load, compatible: [:load]) do + yield + end + end + + def unloading + @lock.exclusive(purpose: :unload, compatible: [:load, :unload]) do + yield + end + end + + # Attempt to obtain an "unloading" (exclusive) lock. If possible, + # execute the supplied block while holding the lock. If there is + # concurrent activity, return immediately (without executing the + # block) instead of waiting. + def attempt_unloading + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], no_wait: true) do + yield + end + end + + def start_running + @lock.start_sharing + end + + def done_running + @lock.stop_sharing + end + + def running + @lock.sharing do + yield + end + end + end + end +end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 0cdc7e96f7..28d2d78643 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -39,8 +39,8 @@ module ActiveSupport } # Behavior module allows to determine how to display deprecation messages. - # You can set any behaviors from +DEFAULT_BEHAVIORS+ constant or create - # custom behavior. Available behaviors: + # You can create a custom behavior or set any from the +DEFAULT_BEHAVIORS+ + # constant. Available behaviors are: # # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>. # [+stderr+] Log all deprecation warnings to +$stderr+. @@ -49,7 +49,7 @@ module ActiveSupport # [+silence+] Do nothing. # # Setting behaviors only affects deprecations that happen after boot time. - # For more information you can read documentation for +behavior=+ method. + # For more information you can read the documentation of the +behavior=+ method. module Behavior # Whether to print a backtrace along with the warning. attr_accessor :debug diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index c6d2b5e795..9b264cbb79 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -127,7 +127,7 @@ module ActiveSupport @deprecator = deprecator end - # Returns class of a new constant. + # Returns the class of the new constant. # # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 4f71f13971..0371f760b0 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -92,7 +92,7 @@ module ActiveSupport # hash = ActiveSupport::HashWithIndifferentAccess.new # hash[:key] = 'value' # - # This value can be later fetched using either +:key+ or +'key'+. + # This value can be later fetched using either +:key+ or <tt>'key'</tt>. def []=(key, value) regular_writer(convert_key(key), convert_value(value, for: :assignment)) end @@ -188,7 +188,7 @@ module ActiveSupport # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| - new_hash.default = default + set_defaults(new_hash) end end @@ -238,16 +238,20 @@ module ActiveSupport def to_options!; self end def select(*args, &block) + return to_enum(:select) unless block_given? dup.tap { |hash| hash.select!(*args, &block) } end def reject(*args, &block) + return to_enum(:reject) unless block_given? dup.tap { |hash| hash.reject!(*args, &block) } end # Convert to a regular hash with string keys. def to_hash - _new_hash = Hash.new(default) + _new_hash = Hash.new + set_defaults(_new_hash) + each do |key, value| _new_hash[key] = convert_value(value, for: :to_hash) end @@ -275,6 +279,14 @@ module ActiveSupport value end end + + def set_defaults(target) + if default_proc + target.default_proc = default_proc.dup + else + target.default = default + end + end end end diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 95f3f6255a..6775eec34b 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -37,10 +37,12 @@ module I18n enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false + reloadable_paths = [] app.config.i18n.each do |setting, value| case setting when :railties_load_path - app.config.i18n.load_path.unshift(*value) + reloadable_paths = value + app.config.i18n.load_path.unshift(*value.map(&:existent).flatten) when :load_path I18n.load_path += value else @@ -53,7 +55,14 @@ module I18n # Restore available locales check so it will take place from now on. I18n.enforce_available_locales = enforce_available_locales - reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } + directories = watched_dirs_with_extensions(reloadable_paths) + reloader = ActiveSupport::FileUpdateChecker.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.reload! + end + app.reloaders << reloader ActionDispatch::Reloader.to_prepare do reloader.execute_if_updated @@ -96,5 +105,11 @@ module I18n raise "Unexpected fallback type #{fallbacks.inspect}" end end + + def self.watched_dirs_with_extensions(paths) + paths.each_with_object({}) do |path, result| + result[path.absolute_current] = path.extensions + end + end end end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 486838bd15..42560f3515 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -27,6 +27,37 @@ module ActiveSupport class Inflections @__instance__ = ThreadSafe::Cache.new + class Uncountables < Array + def initialize + @regex_array = [] + super + end + + def delete(entry) + super entry + @regex_array.delete(to_regex(entry)) + end + + def <<(*word) + add(word) + end + + def add(words) + self.concat(words.flatten.map(&:downcase)) + @regex_array += self.map {|word| to_regex(word) } + self + end + + def uncountable?(str) + @regex_array.any? { |regex| regex === str } + end + + private + def to_regex(string) + /\b#{::Regexp.escape(string)}\Z/i + end + end + def self.instance(locale = :en) @__instance__[locale] ||= new end @@ -34,7 +65,7 @@ module ActiveSupport attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex def initialize - @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/ + @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/ end # Private, for the test suite. @@ -160,7 +191,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - @uncountables += words.flatten.map(&:downcase) + @uncountables.add(words) end # Specifies a humanized form of a string by a regular expression rule or @@ -185,7 +216,7 @@ module ActiveSupport def clear(scope = :all) case scope when :all - @plurals, @singulars, @uncountables, @humans = [], [], [], [] + @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, [] else instance_variable_set "@#{scope}", [] end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index bde70d772d..ab5372ac43 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -68,9 +68,9 @@ module ActiveSupport def camelize(term, uppercase_first_letter = true) string = term.to_s if uppercase_first_letter - string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize } + string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize } else - string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } + string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } string.gsub!('/'.freeze, '::'.freeze) @@ -91,10 +91,10 @@ module ActiveSupport def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) - word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - word.tr!("-", "_") + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) + word.tr!("-".freeze, "_".freeze) word.downcase! word end @@ -127,9 +127,9 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, '') - result.sub!(/_id\z/, '') - result.tr!('_', ' ') + result.sub!(/\A_+/, ''.freeze) + result.sub!(/_id\z/, ''.freeze) + result.tr!('_'.freeze, ' '.freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" @@ -153,14 +153,14 @@ module ActiveSupport # 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]/) { $&.capitalize } + humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize } end # Creates the name of a table like Rails does for models to table names. # This method uses the #pluralize method on the last word in the string. # # tableize('RawScaledScorer') # => "raw_scaled_scorers" - # tableize('egg_and_ham') # => "egg_and_hams" + # tableize('ham_and_egg') # => "ham_and_eggs" # tableize('fancyCategory') # => "fancy_categories" def tableize(class_name) pluralize(underscore(class_name)) @@ -170,7 +170,7 @@ module ActiveSupport # names to models. Note that this returns a string and not a Class (To # convert to an actual class follow +classify+ with #constantize). # - # classify('egg_and_hams') # => "EggAndHam" + # classify('ham_and_eggs') # => "HamAndEgg" # classify('posts') # => "Post" # # Singular names are not handled correctly: @@ -178,14 +178,14 @@ module ActiveSupport # classify('calculus') # => "Calculu" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr('_', '-') + underscored_word.tr('_'.freeze, '-'.freeze) end # Removes the module part from the expression in the string. @@ -248,7 +248,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split('::') + names = camel_cased_word.split('::'.freeze) # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? @@ -354,7 +354,7 @@ module ActiveSupport # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: - parts = camel_cased_word.split("::") + parts = camel_cased_word.split("::".freeze) return Regexp.escape(camel_cased_word) if parts.blank? @@ -372,7 +372,7 @@ module ActiveSupport def apply_inflections(word, rules) result = word.to_s.dup - if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/]) + if word.empty? || inflections.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 edea142e82..7b28eeb6e2 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -58,7 +58,7 @@ module ActiveSupport # I18n.locale = :de # transliterate('Jürgen') # # => "Juergen" - def transliterate(string, replacement = "?") + def transliterate(string, replacement = "?".freeze) I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), :replacement => replacement) @@ -75,13 +75,21 @@ module ActiveSupport # Turn unwanted chars into the separator parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep) unless sep.nil? || sep.empty? - re_sep = Regexp.escape(sep) + if sep == "-".freeze + re_duplicate_seperator = /-{2,}/ + re_leading_trailing_separator = /^-|-$/i + else + re_sep = Regexp.escape(sep) + re_duplicate_seperator = /#{re_sep}{2,}/ + re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i + end # No more than one of the separator in a row. - parameterized_string.gsub!(/#{re_sep}{2,}/, sep) + parameterized_string.gsub!(re_duplicate_seperator, sep) # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) end - parameterized_string.downcase + parameterized_string.downcase! + parameterized_string end end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 48f4967892..031c5e9339 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -57,6 +57,10 @@ module ActiveSupport super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS end end + + def to_s + self + end end # Mark these as private so we don't leak encoding-specific constructs diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 75f353f62c..70ac4a4d5c 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -33,7 +33,7 @@ module ActiveSupport # you can collect them doing @logger.logged(level), where level is the level # used in logging, like info, debug, warn and so on. module TestHelper - def setup + def setup # :nodoc: @logger = MockLogger.new @notifier = ActiveSupport::Notifications::Fanout.new @@ -44,7 +44,7 @@ module ActiveSupport ActiveSupport::Notifications.notifier = @notifier end - def teardown + def teardown # :nodoc: set_logger(nil) ActiveSupport::Notifications.notifier = @old_notifier end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 92ab6fe648..c82a13511e 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -82,7 +82,7 @@ module ActiveSupport def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} + encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index eee9bbaead..64c5232cf4 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -44,9 +44,9 @@ module ActiveSupport # tampered_message = signed_message.chop # editing the message invalidates the signature # verifier.valid_message?(tampered_message) # => false def valid_message?(signed_message) - return if signed_message.blank? + return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--") + data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -74,7 +74,7 @@ module ActiveSupport def verified(signed_message) if valid_message?(signed_message) begin - data = signed_message.split("--")[0] + data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 35efebc65f..b6c991a287 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -58,7 +58,7 @@ module ActiveSupport # 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*' }.join('|') + 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 @@ -273,7 +273,7 @@ module ActiveSupport compose(reorder_characters(decompose(:compatibility, codepoints))) else raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack('U*') + end.pack('U*'.freeze) end def downcase(string) @@ -338,7 +338,7 @@ module ActiveSupport end # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each do |k,_| + @boundary.each_key do |k| @boundary[k].instance_eval do def ===(other) detect { |i| i === other } ? true : false diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 6bf8c7d5de..0131fe2572 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -111,7 +111,7 @@ module ActiveSupport end end - class Timed < Evented + class Timed < Evented # :nodoc: def publish(name, *args) @delegate.call name, *args end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 258d9b34e1..38a9ce361d 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -220,8 +220,6 @@ module ActiveSupport # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes # insignificant zeros after the decimal separator (defaults to # +true+) - # * <tt>:prefix</tt> - If +:si+ formats the number using the SI - # prefix (defaults to :binary) # # ==== Examples # 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 ac0d20b454..a4a8690bcd 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,6 +7,10 @@ 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 diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index bc0326473d..45864990ce 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -6,6 +6,7 @@ module ActiveSupport # h[:girl] = 'Mary' # h[:boy] # => 'John' # h[:girl] # => 'Mary' + # h[:dog] # => nil # # Using +OrderedOptions+, the above code could be reduced to: # @@ -14,6 +15,13 @@ module ActiveSupport # h.girl = 'Mary' # h.boy # => 'John' # h.girl # => 'Mary' + # h.dog # => nil + # + # To raise an exception when the value is blank, append a + # bang to the key name, like: + # + # h.dog! # => raises KeyError + # class OrderedOptions < Hash alias_method :_get, :[] # preserve the original #[] method protected :_get # make it protected diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index d9a668c0ea..ae6f00b861 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -9,6 +9,7 @@ require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' require 'active_support/testing/time_helpers' require 'active_support/testing/file_fixtures' +require 'active_support/testing/composite_filter' require 'active_support/core_ext/kernel/reporting' module ActiveSupport @@ -38,6 +39,15 @@ module ActiveSupport def test_order ActiveSupport.test_order ||= :random end + + def run(reporter, options = {}) + if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ } + options[:filter] = \ + Testing::CompositeFilter.new(self, options[:filter], options[:patterns]) + end + + super + end end alias_method :method_name, :name diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 411dd29df5..d87ce3474d 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -28,7 +28,7 @@ module ActiveSupport # # An arbitrary expression is passed in and evaluated. # - # assert_difference 'assigns(:article).comments(:reload).size' do + # assert_difference 'Article.last.comments(:reload).size' do # post :create, params: { comment: {...} } # end # diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index 5aa5f46310..84c6b89340 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -2,4 +2,11 @@ gem 'minitest' require 'minitest' -Minitest.autorun +if Minitest.respond_to?(:run_with_rails_extension) + unless Minitest.run_with_rails_extension + Minitest.run_with_autorun = true + Minitest.autorun + end +else + Minitest.autorun +end diff --git a/activesupport/lib/active_support/testing/composite_filter.rb b/activesupport/lib/active_support/testing/composite_filter.rb new file mode 100644 index 0000000000..bde723e30b --- /dev/null +++ b/activesupport/lib/active_support/testing/composite_filter.rb @@ -0,0 +1,54 @@ +require 'method_source' + +module ActiveSupport + module Testing + class CompositeFilter # :nodoc: + def initialize(runnable, filter, patterns) + @runnable = runnable + @filters = [ derive_regexp(filter), *derive_line_filters(patterns) ].compact + end + + def ===(method) + @filters.any? { |filter| filter === method } + end + + private + def derive_regexp(filter) + filter =~ %r%/(.*)/% ? Regexp.new($1) : filter + end + + def derive_line_filters(patterns) + patterns.map do |file_and_line| + file, line = file_and_line.split(':') + Filter.new(@runnable, file, line) if file + end + end + + class Filter # :nodoc: + def initialize(runnable, file, line) + @runnable, @file = runnable, File.expand_path(file) + @line = line.to_i if line + end + + def ===(method) + return unless @runnable.method_defined?(method) + + if @line + test_file, test_range = definition_for(@runnable.instance_method(method)) + test_file == @file && test_range.include?(@line) + else + @runnable.instance_method(method).source_location.first == @file + end + end + + private + def definition_for(method) + file, start_line = method.source_location + end_line = method.source.count("\n") + start_line - 1 + + return file, start_line..end_line + end + end + end + end +end diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb new file mode 100644 index 0000000000..155d3344d3 --- /dev/null +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -0,0 +1,39 @@ +module ActiveSupport + module Testing + module MethodCallAssertions # :nodoc: + private + def assert_called(object, method_name, message = nil, times: 1) + times_called = 0 + + object.stub(method_name, proc { times_called += 1 }) { yield } + + error = "Expected #{method_name} to be called #{times} times, " \ + "but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + assert_equal times, times_called, error + end + + def assert_called_with(object, method_name, args = [], returns: nil) + mock = Minitest::Mock.new + + if args.all? { |arg| arg.is_a?(Array) } + args.each { |arg| mock.expect(:call, returns, arg) } + else + mock.expect(:call, returns, args) + end + + object.stub(method_name, mock) { yield } + + mock.verify + end + + def assert_not_called(object, method_name, message = nil, &block) + assert_called(object, method_name, message, times: 0, &block) + end + + def stub_any_instance(klass, instance: klass.new) + klass.stub(:new, instance) { yield instance } + end + end + end +end diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 3478b09423..fca0947c5b 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -39,7 +39,7 @@ module ActiveSupport end end - # Contain helpers that help you test passage of time. + # Contains helpers that help you test passage of time. module TimeHelpers # Changes current time to the time in the future or in the past by a given time difference by # stubbing +Time.now+, +Date.today+, and +DateTime.now+. diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index b0d7f3299f..f8f1b9ac2c 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,3 +1,4 @@ +require 'active_support/duration' require 'active_support/values/time_zone' require 'active_support/core_ext/object/acts_like' @@ -246,7 +247,7 @@ module ActiveSupport end def eql?(other) - utc.eql?(other) + other.eql?(utc) end def hash diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 009ee4db90..df7b081993 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -78,6 +78,9 @@ module ActiveSupport ) end + attr_accessor :depth + self.depth = 100 + delegate :parse, :to => :backend def backend diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index f303daa1a7..94751bbc04 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -46,7 +46,7 @@ module ActiveSupport xml_string_reader = StringReader.new(data) xml_input_source = InputSource.new(xml_string_reader) doc = @dbf.new_document_builder.parse(xml_input_source) - merge_element!({CONTENT_KEY => ''}, doc.document_element) + merge_element!({CONTENT_KEY => ''}, doc.document_element, XmlMini.depth) end end @@ -58,9 +58,10 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) + def merge_element!(hash, element, depth) + raise 'Document too deep!' if depth == 0 delete_empty(hash) - merge!(hash, element.tag_name, collapse(element)) + merge!(hash, element.tag_name, collapse(element, depth)) end def delete_empty(hash) @@ -71,14 +72,14 @@ module ActiveSupport # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) child_nodes = element.child_nodes if child_nodes.length > 0 (0...child_nodes.length).each do |i| child = child_nodes.item(i) - merge_element!(hash, child) unless child.node_type == Node.TEXT_NODE + merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE end merge_texts!(hash, element) unless empty_content?(element) hash diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 5c7c78bf70..924ed72345 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -29,7 +29,7 @@ module ActiveSupport doc = REXML::Document.new(data) if doc.root - merge_element!({}, doc.root) + merge_element!({}, doc.root, XmlMini.depth) else raise REXML::ParseException, "The document #{doc.to_s.inspect} does not have a valid root" @@ -44,19 +44,20 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) - merge!(hash, element.name, collapse(element)) + def merge_element!(hash, element, depth) + raise REXML::ParseException, "The document is too deep" if depth == 0 + merge!(hash, element.name, collapse(element, depth)) end # Actually converts an XML document element into a data structure. # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) if element.has_elements? - element.each_element {|child| merge_element!(hash, child) } + element.each_element {|child| merge_element!(hash, child, depth - 1) } merge_texts!(hash, element) unless empty_content?(element) hash else diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 7ffcae6007..65a8edbabb 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -15,6 +15,7 @@ silence_warnings do end require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' ENV['NO_RELOAD'] = '1' require 'active_support' @@ -37,4 +38,8 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' + +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index dce4e9b051..74ceff44f9 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -133,37 +133,42 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store - Dalli::Client.expects(:new).with(%w[localhost], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_given_mem_cache mem_cache = Dalli::Client.new - Dalli::Client.expects(:new).never - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_not_dalli_client - Dalli::Client.expects(:new).never - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end end end def test_mem_cache_fragment_cache_store_with_multiple_servers - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_options - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 }) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal 'foo', store.options[:namespace] + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { :timeout => 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal 'foo', store.options[:namespace] + end end def test_object_assigned_fragment_cache_store @@ -224,13 +229,15 @@ module CacheStoreBehavior def test_fetch_without_cache_miss @cache.write('foo', 'bar') - @cache.expects(:write).never - assert_equal 'bar', @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_equal 'bar', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss - @cache.expects(:write).with('foo', 'baz', @cache.options) - assert_equal 'baz', @cache.fetch('foo') { 'baz' } + assert_called_with(@cache, :write, ['foo', 'baz', @cache.options]) do + assert_equal 'baz', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss_passes_key_to_block @@ -245,15 +252,18 @@ module CacheStoreBehavior def test_fetch_with_forced_cache_miss @cache.write('foo', 'bar') - @cache.expects(:read).never - @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true)) - @cache.fetch('foo', :force => true) { 'bar' } + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ['foo', 'bar', @cache.options.merge(:force => true)]) do + @cache.fetch('foo', :force => true) { 'bar' } + end + end end def test_fetch_with_cached_nil @cache.write('foo', nil) - @cache.expects(:write).never - assert_nil @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_nil @cache.fetch('foo') { 'baz' } + end end def test_should_read_and_write_hash @@ -288,8 +298,9 @@ module CacheStoreBehavior @cache.write('foo', 'bar', :expires_in => 10) @cache.write('fu', 'baz') @cache.write('fud', 'biz') - Time.stubs(:now).returns(time + 11) - assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) + Time.stub(:now, time + 11) do + assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) + end end def test_fetch_multi @@ -303,8 +314,9 @@ module CacheStoreBehavior end def test_multi_with_objects - foo = stub(:title => 'FOO!', :cache_key => 'foo') - bar = stub(:cache_key => 'bar') + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new('foo', 'FOO!') + bar = cache_struct.new('bar') @cache.write('bar', 'BAM!') @@ -387,66 +399,74 @@ module CacheStoreBehavior def test_expires_in time = Time.local(2008, 4, 24) - Time.stubs(:now).returns(time) - @cache.write('foo', 'bar') - assert_equal 'bar', @cache.read('foo') + Time.stub(:now, time) do + @cache.write('foo', 'bar') + assert_equal 'bar', @cache.read('foo') + end - Time.stubs(:now).returns(time + 30) - assert_equal 'bar', @cache.read('foo') + Time.stub(:now, time + 30) do + assert_equal 'bar', @cache.read('foo') + end - Time.stubs(:now).returns(time + 61) - assert_nil @cache.read('foo') + Time.stub(:now, time + 61) do + assert_nil @cache.read('foo') + end end def test_race_condition_protection_skipped_if_not_defined @cache.write('foo', 'bar') time = @cache.send(:read_entry, 'foo', {}).expires_at - Time.stubs(:now).returns(Time.at(time)) - result = @cache.fetch('foo') do - assert_equal nil, @cache.read('foo') - 'baz' + Time.stub(:now, Time.at(time)) do + result = @cache.fetch('foo') do + assert_equal nil, @cache.read('foo') + 'baz' + end + assert_equal 'baz', result end - assert_equal 'baz', result end def test_race_condition_protection_is_limited time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 71) - result = @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal nil, @cache.read('foo') - "baz" + Time.stub(:now, time + 71) do + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal nil, @cache.read('foo') + "baz" + end + assert_equal "baz", result end - assert_equal "baz", result end def test_race_condition_protection_is_safe time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 61) - begin - @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal 'bar', @cache.read('foo') - raise ArgumentError.new + Time.stub(:now, time + 61) do + begin + @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + raise ArgumentError.new + end + rescue ArgumentError end - rescue ArgumentError + assert_equal "bar", @cache.read('foo') + end + Time.stub(:now, time + 91) do + assert_nil @cache.read('foo') end - assert_equal "bar", @cache.read('foo') - Time.stubs(:now).returns(time + 91) - assert_nil @cache.read('foo') end def test_race_condition_protection time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 61) - result = @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal 'bar', @cache.read('foo') - "baz" + Time.stub(:now, time + 61) do + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + "baz" + end + assert_equal "baz", result end - assert_equal "baz", result end def test_crazy_key_characters @@ -765,9 +785,10 @@ class FileStoreTest < ActiveSupport::TestCase end def test_log_exception_when_cache_read_fails - File.expects(:exist?).raises(StandardError, "failed") - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end end def test_cleanup_removes_all_expired_entries @@ -775,11 +796,12 @@ class FileStoreTest < ActiveSupport::TestCase @cache.write('foo', 'bar', expires_in: 10) @cache.write('baz', 'qux') @cache.write('quux', 'corge', expires_in: 20) - Time.stubs(:now).returns(time + 15) - @cache.cleanup - assert_not @cache.exist?('foo') - assert @cache.exist?('baz') - assert @cache.exist?('quux') + Time.stub(:now, time + 15) do + @cache.cleanup + assert_not @cache.exist?('foo') + assert @cache.exist?('baz') + assert @cache.exist?('quux') + end end def test_write_with_unless_exist @@ -1016,10 +1038,6 @@ class NullStoreTest < ActiveSupport::TestCase end assert_nil @cache.read("name") end - - def test_setting_nil_cache_store - assert ActiveSupport::Cache.lookup_store.class.name, ActiveSupport::Cache::NullStore.name - end end class CacheStoreLoggerTest < ActiveSupport::TestCase @@ -1056,9 +1074,9 @@ class CacheEntryTest < ActiveSupport::TestCase assert !entry.expired?, 'entry not expired' entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60) assert !entry.expired?, 'entry not expired' - time = Time.now + 61 - Time.stubs(:now).returns(time) - assert entry.expired?, 'entry is expired' + Time.stub(:now, Time.now + 61) do + assert entry.expired?, 'entry is expired' + end end def test_compress_values diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 253c1adc23..8ea701cfb7 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -54,6 +54,11 @@ class ConcernTest < ActiveSupport::TestCase include Bar, Baz end + module Qux + module ClassMethods + end + end + def setup @klass = Class.new end @@ -70,6 +75,26 @@ class ConcernTest < ActiveSupport::TestCase assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] end + def test_class_methods_are_extended_only_on_expected_objects + ::Object.__send__(:include, Qux) + Object.extend(Qux::ClassMethods) + # module needs to be created after Qux is included in Object or bug won't + # be triggered + test_module = Module.new do + extend ActiveSupport::Concern + + class_methods do + def test + end + end + end + @klass.include test_module + assert_equal false, Object.respond_to?(:test) + Qux.class_eval do + remove_const :ClassMethods + end + end + def test_included_block_is_ran @klass.include(Baz) assert_equal true, @klass.included_ran diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index e89be25b53..0fc3f765f5 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -191,8 +191,9 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_yesterday_constructor_when_zone_is_set with_env_tz 'UTC' do with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5 - Time.stubs(:now).returns Time.local(2000, 1, 1) - assert_equal Date.new(1999, 12, 30), Date.yesterday + Time.stub(:now, Time.local(2000, 1, 1)) do + assert_equal Date.new(1999, 12, 30), Date.yesterday + end end end end @@ -212,8 +213,9 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_tomorrow_constructor_when_zone_is_set with_env_tz 'UTC' do with_tz_default ActiveSupport::TimeZone['Europe/Paris'] do # UTC +1 - Time.stubs(:now).returns Time.local(1999, 12, 31, 23) - assert_equal Date.new(2000, 1, 2), Date.tomorrow + Time.stub(:now, Time.local(1999, 12, 31, 23)) do + assert_equal Date.new(2000, 1, 2), Date.tomorrow + end end end end @@ -317,23 +319,26 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_past - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_future - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_current_returns_date_today_when_zone_not_set with_env_tz 'US/Central' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23) - assert_equal Date.today, Date.current + Time.stub(:now, Time.local(1999, 12, 31, 23)) do + assert_equal Date.today, Date.current + end end end diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 74319ecd09..6fe38c45ec 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -204,61 +204,69 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_today_with_offset - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_today_without_offset - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_past_with_offset - DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - 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.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - 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.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - 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.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - 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 def test_current_returns_date_today_when_zone_is_not_set with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) - assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end end end def test_current_returns_time_zone_today_when_zone_is_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) - assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end end ensure Time.zone = nil @@ -335,6 +343,13 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase 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" + end + 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 diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index c283b546e6..9e97acaffb 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -140,28 +140,30 @@ class DurationTest < ActiveSupport::TestCase def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set Time.zone = nil with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_not_instance_of ActiveSupport::TimeWithZone, 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 + 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 + # ago + assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago + assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago + end end end def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # 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 '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 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name + 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 '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 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name + end end ensure Time.zone = nil diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 21743cdea5..f09b7d8850 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -3,6 +3,8 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/enumerable' Payment = Struct.new(:price) +ExpandedPayment = Struct.new(:dollars, :cents) + class SummablePayment < Payment def +(p) self.class.new(price + p.price) end end @@ -114,5 +116,12 @@ class EnumerableTests < ActiveSupport::TestCase def test_pluck payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) assert_equal [5, 15, 10], payments.pluck(:price) + + payments = GenericEnumerable.new([ + ExpandedPayment.new(5, 99), + ExpandedPayment.new(15, 0), + ExpandedPayment.new(10, 50) + ]) + assert_equal [[5, 99], [15, 0], [10, 50]], payments.pluck(:dollars, :cents) end end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index e10bee5e00..4624338df1 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -524,6 +524,10 @@ class HashExtTest < ActiveSupport::TestCase 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] @@ -547,6 +551,11 @@ class HashExtTest < ActiveSupport::TestCase 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} @@ -568,6 +577,11 @@ class HashExtTest < ActiveSupport::TestCase 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} @@ -961,10 +975,11 @@ class HashExtTest < ActiveSupport::TestCase assert_raise(RuntimeError) { original.except!(:a) } end - def test_except_with_mocha_expectation_on_original + def test_except_does_not_delete_values_in_original original = { :a => 'x', :b => 'y' } - original.expects(:delete).never - original.except(:a) + assert_not_called(original, :delete) do + original.except(:a) + end end def test_compact @@ -998,6 +1013,37 @@ class HashExtTest < ActiveSupport::TestCase 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_existant] + 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_nothing_raised { HashWithIndifferentAccess.new_from_hash_copying_default(hash) } + end end class IWriteMyOwnXML diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 48f3cc579f..128c5e3d1a 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -76,4 +76,10 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal 'default_reader_value', @module.defr assert_equal 'default_writer_value', @module.class_variable_get('@@defw') end + + def test_should_not_invoke_default_value_block_multiple_times + count = 0 + @module.cattr_accessor(:defcount){ count += 1 } + assert_equal 1, count + end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 2d8796179e..0ff8f0f89b 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -280,14 +280,16 @@ class NumericExtFormattingTest < ActiveSupport::TestCase end def test_to_s__human_size_with_si_prefix - 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_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) + end end def test_to_s__human_size_with_options_hash diff --git a/activesupport/test/core_ext/object/json_gem_encoding_test.rb b/activesupport/test/core_ext/object/json_gem_encoding_test.rb new file mode 100644 index 0000000000..02ab17fb64 --- /dev/null +++ b/activesupport/test/core_ext/object/json_gem_encoding_test.rb @@ -0,0 +1,66 @@ +require 'abstract_unit' +require 'json' +require 'json/encoding_test_cases' + +# These test cases were added to test that we do not interfere with json gem's +# output when the AS encoder is loaded, primarily for problems reported in +# #20775. They need to be executed in isolation to reproduce the scenario +# correctly, because other test cases might have already loaded additional +# dependencies. + +# The AS::JSON encoder requires the BigDecimal core_ext, which, unfortunately, +# changes the BigDecimal#to_s output, and consequently the JSON gem output. So +# we need to require this unfront to ensure we don't get a false failure, but +# ideally we should just fix the BigDecimal core_ext to not change to_s without +# arguments. +require 'active_support/core_ext/big_decimal' + +class JsonGemEncodingTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + JSONTest::EncodingTestCases.constants.each_with_index do |name| + JSONTest::EncodingTestCases.const_get(name).each_with_index do |(subject, _), i| + test("#{name[0..-6].underscore} #{i}") do + assert_same_with_or_without_active_support(subject) + end + end + end + + class CustomToJson + def to_json(*) + '"custom"' + end + end + + test "custom to_json" do + assert_same_with_or_without_active_support(CustomToJson.new) + end + + private + def require_or_skip(file) + require(file) || skip("'#{file}' was already loaded") + end + + def assert_same_with_or_without_active_support(subject) + begin + expected = JSON.generate(subject, quirks_mode: true) + rescue JSON::GeneratorError => e + exception = e + end + + require_or_skip 'active_support/core_ext/object/json' + + if exception + assert_raises_with_message JSON::GeneratorError, e.message do + JSON.generate(subject, quirks_mode: true) + end + else + assert_equal expected, JSON.generate(subject, quirks_mode: true) + end + end + + def assert_raises_with_message(exception_class, message, &block) + err = assert_raises(exception_class) { block.call } + assert_match message, err.message + end +end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index cb24147ab3..3a5d6df06d 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -435,117 +435,123 @@ class StringConversionsTest < ActiveSupport::TestCase def test_standard_time_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end end end def test_standard_time_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end end end def test_daylight_savings_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end end end def test_daylight_savings_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end end end def test_partial_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time - assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time - assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time - assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc) + end end end def test_partial_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time - assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time - assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time - assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time - assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc) + end end end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index d59775001b..b14c04fba6 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -149,6 +149,9 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase 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 + end end def test_end_of_hour @@ -389,6 +392,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase 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(ArgumentError) { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) } end def test_utc_change @@ -601,13 +605,15 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_days_in_month_feb_in_common_year_without_year_arg - Time.stubs(:now).returns(Time.utc(2007)) - assert_equal 28, Time.days_in_month(2) + Time.stub(:now, Time.utc(2007)) do + assert_equal 28, Time.days_in_month(2) + end end def test_days_in_month_feb_in_leap_year_without_year_arg - Time.stubs(:now).returns(Time.utc(2008)) - assert_equal 29, Time.days_in_month(2) + Time.stub(:now, Time.utc(2008)) do + assert_equal 29, Time.days_in_month(2) + end end def test_last_month_on_31st @@ -619,68 +625,74 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_today_with_time_local - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_today_with_time_utc - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_past_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - 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)') - Time.stubs(:current).returns(twz) - 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? + 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? + end end end def test_future_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - 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)') - Time.stubs(:current).returns(twz) - 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? + 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? + end end end @@ -721,6 +733,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase 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' + end + def test_at_with_datetime assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0)) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 79d78c02cd..ccb7f02331 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' require 'active_support/core_ext/string/strip' +require 'yaml' class TimeWithZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -205,52 +206,61 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_today - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - 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? + 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? + end end def test_past_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - 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) ) - Time.stubs(:current).returns(twz) - 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, 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? + end end def test_future_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - 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) ) - Time.stubs(:current).returns(twz) - 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, 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? + 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) ) + + other_twz = ActiveSupport::TimeWithZone.new(DateTime.now.utc, @time_zone) + assert_equal true, other_twz.eql?(other_twz.dup) end def test_hash @@ -477,22 +487,24 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_method_missing_with_non_time_return_value - @twz.time.expects(:foo).returns('bar') + time = @twz.time + def time.foo; 'bar'; end assert_equal 'bar', @twz.foo end def test_date_part_value_methods twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone) - twz.expects(:method_missing).never - assert_equal 1999, twz.year - assert_equal 12, twz.month - assert_equal 31, twz.day - assert_equal 14, twz.hour - assert_equal 18, twz.min - assert_equal 17, twz.sec - assert_equal 500, twz.usec - assert_equal 5, twz.wday - assert_equal 365, twz.yday + assert_not_called(twz, :method_missing) do + assert_equal 1999, twz.year + assert_equal 12, twz.month + assert_equal 31, twz.day + assert_equal 14, twz.hour + assert_equal 18, twz.min + assert_equal 17, twz.sec + assert_equal 500, twz.usec + assert_equal 5, twz.wday + assert_equal 365, twz.yday + end end def test_usec_returns_0_when_datetime_is_wrapped @@ -1033,19 +1045,21 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_current_returns_time_now_when_zone_not_set with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.local(2000), Time.current + Time.stub(:now, Time.local(2000)) do + assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone) + assert_equal Time.local(2000), Time.current + end end end def test_current_returns_time_zone_now_when_zone_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone) - assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name - assert_equal Time.utc(2000), Time.current.time + Time.stub(:now, Time.local(2000)) do + assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone) + assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name + assert_equal Time.utc(2000), Time.current.time + end end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 6dce7560dd..757e600646 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -721,12 +721,13 @@ class DependenciesTest < ActiveSupport::TestCase end def test_unloadable_constants_should_receive_callback - Object.const_set :C, Class.new + Object.const_set :C, Class.new { def self.before_remove_const; end } C.unloadable - C.expects(:before_remove_const).once - assert C.respond_to?(:before_remove_const) - ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_called(C, :before_remove_const, times: 1) do + assert C.respond_to?(:before_remove_const) + ActiveSupport::Dependencies.clear + assert !defined?(C) + end ensure remove_constants(:C) end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 20bd8ee5dd..7e8844b301 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -256,17 +256,17 @@ class DeprecationTest < ActiveSupport::TestCase end def test_deprecate_with_custom_deprecator - custom_deprecator = mock('Deprecator') do - expects(:deprecation_warning) - end + custom_deprecator = Struct.new(:deprecation_warning).new - klass = Class.new do - def method + assert_called_with(custom_deprecator, :deprecation_warning, [:method, nil]) do + klass = Class.new do + def method + end + deprecate :method, deprecator: custom_deprecator end - deprecate :method, deprecator: custom_deprecator - end - klass.new.method + klass.new.method + end end def test_deprecated_constant_with_deprecator_given diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb deleted file mode 100644 index 1facd691fa..0000000000 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'abstract_unit' -require 'active_support/hash_with_indifferent_access' - -class HashWithIndifferentAccessTest < ActiveSupport::TestCase - def test_reverse_merge - hash = HashWithIndifferentAccess.new key: :old_value - hash.reverse_merge! key: :new_value - assert_equal :old_value, hash[:key] - end - -end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index be68bb2e2e..a0764f6d6b 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -8,6 +8,20 @@ class InflectorTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases + def setup + # Dups the singleton before each test, restoring the original inflections later. + # + # This helper is implemented by setting @__instance__ because in some tests + # there are module functions that access ActiveSupport::Inflector.inflections, + # so we need to replace the singleton itself. + @original_inflections = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections.dup) + end + + def teardown + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections) + end + def test_pluralize_plurals assert_equal "plurals", ActiveSupport::Inflector.pluralize("plurals") assert_equal "Plurals", ActiveSupport::Inflector.pluralize("Plurals") @@ -26,20 +40,18 @@ class InflectorTest < ActiveSupport::TestCase end def test_uncountable_word_is_not_greedy - with_dup do - uncountable_word = "ors" - countable_word = "sponsor" + uncountable_word = "ors" + countable_word = "sponsor" - ActiveSupport::Inflector.inflections.uncountable << uncountable_word + ActiveSupport::Inflector.inflections.uncountable << uncountable_word - assert_equal uncountable_word, ActiveSupport::Inflector.singularize(uncountable_word) - assert_equal uncountable_word, ActiveSupport::Inflector.pluralize(uncountable_word) - assert_equal ActiveSupport::Inflector.pluralize(uncountable_word), ActiveSupport::Inflector.singularize(uncountable_word) + assert_equal uncountable_word, ActiveSupport::Inflector.singularize(uncountable_word) + assert_equal uncountable_word, ActiveSupport::Inflector.pluralize(uncountable_word) + assert_equal ActiveSupport::Inflector.pluralize(uncountable_word), ActiveSupport::Inflector.singularize(uncountable_word) - assert_equal "sponsor", ActiveSupport::Inflector.singularize(countable_word) - assert_equal "sponsors", ActiveSupport::Inflector.pluralize(countable_word) - assert_equal "sponsor", ActiveSupport::Inflector.singularize(ActiveSupport::Inflector.pluralize(countable_word)) - end + assert_equal "sponsor", ActiveSupport::Inflector.singularize(countable_word) + assert_equal "sponsors", ActiveSupport::Inflector.pluralize(countable_word) + assert_equal "sponsor", ActiveSupport::Inflector.singularize(ActiveSupport::Inflector.pluralize(countable_word)) end SingularToPlural.each do |singular, plural| @@ -70,11 +82,9 @@ class InflectorTest < ActiveSupport::TestCase def test_overwrite_previous_inflectors - with_dup do - assert_equal("series", ActiveSupport::Inflector.singularize("series")) - ActiveSupport::Inflector.inflections.singular "series", "serie" - assert_equal("serie", ActiveSupport::Inflector.singularize("series")) - end + assert_equal("series", ActiveSupport::Inflector.singularize("series")) + ActiveSupport::Inflector.inflections.singular "series", "serie" + assert_equal("serie", ActiveSupport::Inflector.singularize("series")) end MixtureToTitleCase.each_with_index do |(before, titleized), index| @@ -367,10 +377,8 @@ class InflectorTest < ActiveSupport::TestCase %w{plurals singulars uncountables humans}.each do |inflection_type| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def test_clear_#{inflection_type} - with_dup do - ActiveSupport::Inflector.inflections.clear :#{inflection_type} - assert ActiveSupport::Inflector.inflections.#{inflection_type}.empty?, \"#{inflection_type} inflections should be empty after clear :#{inflection_type}\" - end + ActiveSupport::Inflector.inflections.clear :#{inflection_type} + assert ActiveSupport::Inflector.inflections.#{inflection_type}.empty?, \"#{inflection_type} inflections should be empty after clear :#{inflection_type}\" end RUBY end @@ -405,73 +413,63 @@ class InflectorTest < ActiveSupport::TestCase end def test_clear_all - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - # ensure any data is present - inflect.plural(/(quiz)$/i, '\1zes') - inflect.singular(/(database)s$/i, '\1') - inflect.uncountable('series') - inflect.human("col_rpted_bugs", "Reported bugs") - - inflect.clear :all - - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? - end + ActiveSupport::Inflector.inflections do |inflect| + # ensure any data is present + inflect.plural(/(quiz)$/i, '\1zes') + inflect.singular(/(database)s$/i, '\1') + inflect.uncountable('series') + inflect.human("col_rpted_bugs", "Reported bugs") + + inflect.clear :all + + assert inflect.plurals.empty? + assert inflect.singulars.empty? + assert inflect.uncountables.empty? + assert inflect.humans.empty? end end def test_clear_with_default - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - # ensure any data is present - inflect.plural(/(quiz)$/i, '\1zes') - inflect.singular(/(database)s$/i, '\1') - inflect.uncountable('series') - inflect.human("col_rpted_bugs", "Reported bugs") - - inflect.clear - - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? - end + ActiveSupport::Inflector.inflections do |inflect| + # ensure any data is present + inflect.plural(/(quiz)$/i, '\1zes') + inflect.singular(/(database)s$/i, '\1') + inflect.uncountable('series') + inflect.human("col_rpted_bugs", "Reported bugs") + + inflect.clear + + assert inflect.plurals.empty? + assert inflect.singulars.empty? + assert inflect.uncountables.empty? + assert inflect.humans.empty? end end Irregularities.each do |singular, plural| define_method("test_irregularity_between_#{singular}_and_#{plural}") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(singular) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(singular) end end end Irregularities.each do |singular, plural| define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(plural) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(plural) end end end Irregularities.each do |singular, plural| define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(singular) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(singular) end end end @@ -503,12 +501,10 @@ class InflectorTest < ActiveSupport::TestCase %w(plurals singulars uncountables humans acronyms).each do |scope| define_method("test_clear_inflections_with_#{scope}") do - with_dup do - # clear the inflections - ActiveSupport::Inflector.inflections do |inflect| - inflect.clear(scope) - assert_equal [], inflect.send(scope) - end + # clear the inflections + ActiveSupport::Inflector.inflections do |inflect| + inflect.clear(scope) + assert_equal [], inflect.send(scope) end end end @@ -520,18 +516,4 @@ class InflectorTest < ActiveSupport::TestCase assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") end - - # Dups the singleton and yields, restoring the original inflections later. - # Use this in tests what modify the state of the singleton. - # - # This helper is implemented by setting @__instance__ because in some tests - # there are module functions that access ActiveSupport::Inflector.inflections, - # so we need to replace the singleton itself. - def with_dup - original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) - yield - ensure - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) - end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 2f269a66f0..9f4b62fd8b 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -4,122 +4,24 @@ require 'active_support/core_ext/string/inflections' require 'active_support/json' require 'active_support/time' require 'time_zone_test_helpers' +require 'json/encoding_test_cases' class TestJSONEncoding < ActiveSupport::TestCase include TimeZoneTestHelpers - class Foo - def initialize(a, b) - @a, @b = a, b - end - end - - class Hashlike - def to_hash - { :foo => "hello", :bar => "world" } - end - end - - class Custom - def initialize(serialized) - @serialized = serialized - end - - def as_json(options = nil) - @serialized - end - end - - class CustomWithOptions - attr_accessor :foo, :bar - - def as_json(options={}) - options[:only] = %w(foo bar) - super(options) - end - end - - class OptionsTest - def as_json(options = :default) - options - end - end - - class HashWithAsJson < Hash - attr_accessor :as_json_called - - def initialize(*) - super - end - - def as_json(options={}) - @as_json_called = true - super - end - end - - TrueTests = [[ true, %(true) ]] - FalseTests = [[ false, %(false) ]] - 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) ], - [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] - - StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], - [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], - [ 'http://test.host/posts/1', %("http://test.host/posts/1")], - [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", - %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] - - ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], - [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] - - RangeTests = [[ 1..2, %("1..2")], - [ 1...2, %("1...2")], - [ 1.5..2.5, %("1.5..2.5")]] - - SymbolTests = [[ :a, %("a") ], - [ :this, %("this") ], - [ :"a b", %("a b") ]] - - ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] - HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] - CustomTests = [[ Custom.new("custom"), '"custom"' ], - [ Custom.new(nil), 'null' ], - [ Custom.new(:a), '"a"' ], - [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], - [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], - [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], - [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] - - RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] - - 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") ]] - StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]] - def sorted_json(json) return json unless json =~ /^\{.*\}$/ '{' + json[1..-2].split(',').sort.join(',') + '}' end - constants.grep(/Tests$/).each do |class_tests| + JSONTest::EncodingTestCases.constants.each do |class_tests| define_method("test_#{class_tests[0..-6].underscore}") do begin prev = ActiveSupport.use_standard_json_time_format ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/ ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/ - self.class.const_get(class_tests).each do |pair| + JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) end ensure @@ -147,6 +49,13 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d)) end + def test_hash_keys_encoding + ActiveSupport.escape_html_entities_in_json = true + assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>") + ensure + ActiveSupport.escape_html_entities_in_json = false + end + def test_utf8_string_encoded_properly result = ActiveSupport::JSON.encode('€2.99') assert_equal '"€2.99"', result @@ -217,7 +126,7 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_hash_like_with_options - h = Hashlike.new + h = JSONTest::Hashlike.new json = h.to_json :only => [:foo] assert_equal({"foo"=>"hello"}, JSON.parse(json)) @@ -338,6 +247,15 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end + class CustomWithOptions + attr_accessor :foo, :bar + + def as_json(options={}) + options[:only] = %w(foo bar) + super(options) + end + end + def test_hash_to_json_should_not_keep_options_around f = CustomWithOptions.new f.foo = "hello" @@ -358,6 +276,12 @@ class TestJSONEncoding < ActiveSupport::TestCase {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json)) end + class OptionsTest + def as_json(options = :default) + options + end + end + def test_hash_as_json_without_options json = { foo: OptionsTest.new }.as_json assert_equal({"foo" => :default}, json) @@ -405,6 +329,19 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal false, false.as_json end + class HashWithAsJson < Hash + attr_accessor :as_json_called + + def initialize(*) + super + end + + def as_json(options={}) + @as_json_called = true + super + end + end + def test_json_gem_dump_by_passing_active_support_encoder h = HashWithAsJson.new h[:foo] = "hello" diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb new file mode 100644 index 0000000000..0159ba8606 --- /dev/null +++ b/activesupport/test/json/encoding_test_cases.rb @@ -0,0 +1,88 @@ +require 'bigdecimal' + +module JSONTest + class Foo + def initialize(a, b) + @a, @b = a, b + end + end + + class Hashlike + def to_hash + { :foo => "hello", :bar => "world" } + end + end + + class Custom + def initialize(serialized) + @serialized = serialized + end + + def as_json(options = nil) + @serialized + end + end + + class MyStruct < Struct.new(:name, :value) + def initialize(*) + @unused = "unused instance variable" + super + end + end + + module EncodingTestCases + TrueTests = [[ true, %(true) ]] + FalseTests = [[ false, %(false) ]] + 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) ], + [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] + + StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], + [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], + [ 'http://test.host/posts/1', %("http://test.host/posts/1")], + [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", + %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] + + ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], + [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] + + HashTests = [[ {foo: "bar"}, %({\"foo\":\"bar\"}) ], + [ {1 => 1, 2 => 'a', 3 => :b, 4 => nil, 5 => false}, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]] + + RangeTests = [[ 1..2, %("1..2")], + [ 1...2, %("1...2")], + [ 1.5..2.5, %("1.5..2.5")]] + + SymbolTests = [[ :a, %("a") ], + [ :this, %("this") ], + [ :"a b", %("a b") ]] + + ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] + HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] + StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ], + [ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]] + CustomTests = [[ Custom.new("custom"), '"custom"' ], + [ Custom.new(nil), 'null' ], + [ Custom.new(:a), '"a"' ], + [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], + [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] + + RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] + + 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") ]] + StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]] + end +end diff --git a/activesupport/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb index 2a0e8d20ed..998a6887c5 100644 --- a/activesupport/test/log_subscriber_test.rb +++ b/activesupport/test/log_subscriber_test.rb @@ -82,10 +82,11 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase def test_does_not_send_the_event_if_logger_is_nil ActiveSupport::LogSubscriber.logger = nil - @log_subscriber.expects(:some_event).never - ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber - instrument "some_event.my_log_subscriber" - wait + assert_not_called(@log_subscriber, :some_event) do + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + instrument "some_event.my_log_subscriber" + wait + end end def test_does_not_fail_with_non_namespaced_events diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 6c3519df9a..668d78492e 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -24,6 +24,7 @@ class MessageVerifierTest < ActiveSupport::TestCase data, hash = @verifier.generate(@data).split("--") assert !@verifier.valid_message?(nil) assert !@verifier.valid_message?("") + assert !@verifier.valid_message?("\xff") # invalid encoding assert !@verifier.valid_message?("#{data.reverse}--#{hash}") assert !@verifier.valid_message?("#{data}--#{hash.reverse}") assert !@verifier.valid_message?("purejunk") diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb index 52ae266f01..dd33641ec2 100644 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ b/activesupport/test/multibyte_unicode_database_test.rb @@ -11,8 +11,9 @@ class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase UnicodeDatabase::ATTRIBUTES.each do |attribute| define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do - @ucd.expects(:load) - @ucd.send(attribute) + assert_called(@ucd, :load) do + @ucd.send(attribute) + end end end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 83efbffdfb..cb5230c4eb 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -234,15 +234,17 @@ module ActiveSupport end def test_number_to_human_size_with_si_prefix - [@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_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) + end end end diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb new file mode 100644 index 0000000000..ad41db608b --- /dev/null +++ b/activesupport/test/share_lock_test.rb @@ -0,0 +1,333 @@ +require 'abstract_unit' +require 'concurrent/atomics' +require 'active_support/concurrency/share_lock' + +class ShareLockTest < ActiveSupport::TestCase + def setup + @lock = ActiveSupport::Concurrency::ShareLock.new + end + + def test_reentrancy + thread = Thread.new do + @lock.sharing { @lock.sharing {} } + @lock.exclusive { @lock.exclusive {} } + end + assert_threads_not_stuck thread + end + + def test_sharing_doesnt_block + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch| + assert_threads_not_stuck(Thread.new {@lock.sharing {} }) + end + end + + def test_sharing_blocks_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + @lock.exclusive(no_wait: true) { flunk } # polling should fail + exclusive_thread = Thread.new { @lock.exclusive {} } + assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch + end + end + + def test_exclusive_blocks_sharing + with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch| + sharing_thread = Thread.new { @lock.sharing {} } + assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch + end + end + + def test_multiple_exlusives_are_able_to_progress + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + exclusive_threads = (1..2).map do + Thread.new do + @lock.exclusive {} + end + end + + assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch + end + end + + def test_sharing_is_upgradeable_to_exclusive + upgrading_thread = Thread.new do + @lock.sharing do + @lock.exclusive {} + end + end + assert_threads_not_stuck upgrading_thread + end + + def test_exclusive_upgrade_waits_for_other_sharers_to_leave + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + in_sharing = Concurrent::CountDownLatch.new + + upgrading_thread = Thread.new do + @lock.sharing do + in_sharing.count_down + @lock.exclusive {} + end + end + + in_sharing.wait + assert_threads_stuck_but_releasable_by_latch upgrading_thread, sharing_thread_release_latch + end + end + + def test_exclusive_matching_purpose + [true, false].each do |use_upgrading| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + exclusive_threads = (1..2).map do + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {} + end + end + end + + assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch + end + end + end + + def test_killed_thread_loses_lock + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + thread = Thread.new do + @lock.sharing do + @lock.exclusive {} + end + end + + assert_threads_stuck thread + thread.kill + + sharing_thread_release_latch.count_down + + thread = Thread.new do + @lock.exclusive {} + end + + assert_threads_not_stuck thread + end + end + + def test_exclusive_conflicting_purpose + [true, false].each do |use_upgrading| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + begin + conflicting_exclusive_threads = [ + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: :red, compatible: [:green, :purple]) {} + end + end, + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: :blue, compatible: [:green]) {} + end + end + ] + + assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks + + # This thread will be stuck as long as any other thread is in + # a sharing block. While it's blocked, it holds no lock, so it + # doesn't interfere with any other attempts. + no_purpose_thread = Thread.new do + @lock.exclusive {} + end + assert_threads_stuck no_purpose_thread + + # This thread is compatible with both of the "primary" + # attempts above. It's initially stuck on the outer share + # lock, but as soon as that's released, it can run -- + # regardless of whether those threads hold share locks. + compatible_thread = Thread.new do + @lock.exclusive(purpose: :green, compatible: []) {} + end + assert_threads_stuck compatible_thread + + assert_threads_stuck conflicting_exclusive_threads + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through + + if use_upgrading + # The "primary" threads both each hold a share lock, and are + # mutually incompatible; they're still stuck. + assert_threads_stuck conflicting_exclusive_threads + + # The thread without a specified purpose is also stuck; it's + # not compatible with anything. + assert_threads_stuck no_purpose_thread + else + # As the primaries didn't hold a share lock, as soon as the + # outer one was released, all the exclusive locks are free + # to be acquired in turn. + + assert_threads_not_stuck conflicting_exclusive_threads + assert_threads_not_stuck no_purpose_thread + end + ensure + conflicting_exclusive_threads.each(&:kill) + no_purpose_thread.kill + end + end + end + end + + def test_exclusive_ordering + scratch_pad = [] + scratch_pad_mutex = Mutex.new + + load_params = [:load, [:load]] + unload_params = [:unload, [:unload, :load]] + + [load_params, load_params, unload_params, unload_params].permutation do |thread_params| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + threads = thread_params.map do |purpose, compatible| + Thread.new do + @lock.sharing do + @lock.exclusive(purpose: purpose, compatible: compatible) do + scratch_pad_mutex.synchronize { scratch_pad << purpose } + end + end + end + end + + sleep(0.01) + scratch_pad_mutex.synchronize { assert_empty scratch_pad } + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck threads + scratch_pad_mutex.synchronize do + assert_equal [:load, :load, :unload, :unload], scratch_pad + scratch_pad.clear + end + end + end + end + + def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads + scratch_pad = [] + scratch_pad_mutex = Mutex.new + + upgrading_load_params = [:load, [:load], true] + non_upgrading_unload_params = [:unload, [:load, :unload], false] + + [upgrading_load_params, non_upgrading_unload_params].permutation do |thread_params| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + threads = thread_params.map do |purpose, compatible, use_upgrading| + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: purpose, compatible: compatible) do + scratch_pad_mutex.synchronize { scratch_pad << purpose } + end + end + end + end + + assert_threads_stuck threads + scratch_pad_mutex.synchronize { assert_empty scratch_pad } + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck threads + scratch_pad_mutex.synchronize do + assert_equal [:load, :unload], scratch_pad + scratch_pad.clear + end + end + end + end + + private + + module CustomAssertions + SUFFICIENT_TIMEOUT = 0.2 + + private + + def assert_threads_stuck_but_releasable_by_latch(threads, latch) + assert_threads_stuck threads + latch.count_down + assert_threads_not_stuck threads + end + + def assert_threads_stuck(threads) + sleep(SUFFICIENT_TIMEOUT) # give threads time to do their business + assert(Array(threads).all? { |t| t.join(0.001).nil? }) + end + + def assert_threads_not_stuck(threads) + assert(Array(threads).all? { |t| t.join(SUFFICIENT_TIMEOUT) }) + end + end + + class CustomAssertionsTest < ActiveSupport::TestCase + include CustomAssertions + + def setup + @latch = Concurrent::CountDownLatch.new + @thread = Thread.new { @latch.wait } + end + + def teardown + @latch.count_down + @thread.join + end + + def test_happy_path + assert_threads_stuck_but_releasable_by_latch @thread, @latch + end + + def test_detects_stuck_thread + assert_raises(Minitest::Assertion) do + assert_threads_not_stuck @thread + end + end + + def test_detects_free_thread + @latch.count_down + assert_raises(Minitest::Assertion) do + assert_threads_stuck @thread + end + end + + def test_detects_already_released + @latch.count_down + assert_raises(Minitest::Assertion) do + assert_threads_stuck_but_releasable_by_latch @thread, @latch + end + end + + def test_detects_remains_latched + another_latch = Concurrent::CountDownLatch.new + assert_raises(Minitest::Assertion) do + assert_threads_stuck_but_releasable_by_latch @thread, another_latch + end + end + end + + include CustomAssertions + + def with_thread_waiting_in_lock_section(lock_section) + in_section = Concurrent::CountDownLatch.new + section_release = Concurrent::CountDownLatch.new + + stuck_thread = Thread.new do + @lock.send(lock_section) do + in_section.count_down + section_release.wait + end + end + + in_section.wait + + yield section_release + ensure + section_release.count_down + stuck_thread.join # clean up + end +end diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb new file mode 100644 index 0000000000..2939cf0233 --- /dev/null +++ b/activesupport/test/testing/method_call_assertions_test.rb @@ -0,0 +1,117 @@ +require 'abstract_unit' +require 'active_support/testing/method_call_assertions' + +class MethodCallAssertionsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + class Level + def increment; 1; end + def decrement; end + def <<(arg); end + end + + setup do + @object = Level.new + end + + def test_assert_called_with_defaults_to_expect_once + assert_called @object, :increment do + @object.increment + end + end + + def test_assert_called_more_than_once + assert_called(@object, :increment, times: 2) do + @object.increment + @object.increment + end + end + + def test_assert_called_method_with_arguments + assert_called(@object, :<<) do + @object << 2 + end + end + + def test_assert_called_failure + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment) do + # Call nothing... + end + end + + assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message + end + + def test_assert_called_with_message + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment, 'dang it') do + # Call nothing... + end + end + + assert_match(/dang it.\nExpected increment/, error.message) + end + + def test_assert_called_with + assert_called_with(@object, :increment) do + @object.increment + end + end + + def test_assert_called_with_arguments + assert_called_with(@object, :<<, [ 2 ]) do + @object << 2 + end + end + + def test_assert_called_with_failure + assert_raises(MockExpectationError) do + assert_called_with(@object, :<<, [ 4567 ]) do + @object << 2 + end + end + end + + def test_assert_called_with_returns + assert_called_with(@object, :increment, returns: 1) do + @object.increment + end + end + + def test_assert_called_with_multiple_expected_arguments + assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do + @object << 1 + @object << 2 + end + end + + def test_assert_not_called + assert_not_called(@object, :decrement) do + @object.increment + end + end + + def test_assert_not_called_failure + error = assert_raises(Minitest::Assertion) do + assert_not_called(@object, :increment) do + @object.increment + end + end + + assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message + end + + def test_stub_any_instance + stub_any_instance(Level) do |instance| + assert_equal instance, Level.new + end + end + + def test_stub_any_instance_with_instance + stub_any_instance(Level, instance: @object) do |instance| + assert_equal @object, instance + assert_equal instance, Level.new + end + end +end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 869bc09991..59c3e52c2f 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -3,80 +3,88 @@ require 'active_support/core_ext/date_time' require 'active_support/core_ext/numeric/time' class TimeTravelTest < ActiveSupport::TestCase - setup do - Time.stubs now: Time.now - end - teardown do travel_back end def test_time_helper_travel - expected_time = Time.now + 1.day - travel 1.day - - assert_equal expected_time.to_s(:db), Time.now.to_s(:db) - assert_equal expected_time.to_date, Date.today - assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) - end - - def test_time_helper_travel_with_block - expected_time = Time.now + 1.day + Time.stub(:now, Time.now) do + expected_time = Time.now + 1.day + travel 1.day - travel 1.day do assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end - - assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) - assert_not_equal expected_time.to_date, Date.today - assert_not_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end - def test_time_helper_travel_to - expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time - - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now + def test_time_helper_travel_with_block + Time.stub(:now, Time.now) do + expected_time = Time.now + 1.day + + travel 1.day do + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_equal expected_time.to_date, Date.today + assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) + end + + assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_not_equal expected_time.to_date, Date.today + assert_not_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) + end end - def test_time_helper_travel_to_with_block - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + def test_time_helper_travel_to + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + travel_to expected_time - travel_to expected_time do assert_equal expected_time, Time.now assert_equal Date.new(2004, 11, 24), Date.today assert_equal expected_time.to_datetime, DateTime.now end + end - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today - assert_not_equal expected_time.to_datetime, DateTime.now + def test_time_helper_travel_to_with_block + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time do + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + end + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now + end end def test_time_helper_travel_back - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now - travel_back + travel_to expected_time + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + travel_back - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today - assert_not_equal expected_time.to_datetime, DateTime.now + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now + end end def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding - travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do - assert_equal 50, Time.now.sec - assert_equal 0, Time.now.usec - assert_equal 50, DateTime.now.sec - assert_equal 0, DateTime.now.usec + Time.stub(:now, Time.now) do + travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do + assert_equal 50, Time.now.sec + assert_equal 0, Time.now.usec + assert_equal 50, DateTime.now.sec + assert_equal 0, DateTime.now.usec + end end end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 5e0474f449..00d40c4497 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'yaml' class TimeZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -252,9 +253,10 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_incomplete_date zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - zone.stubs(:now).returns zone.local(1999,12,31) - twz = zone.parse('19:00:00') - assert_equal Time.utc(1999,12,31,19), twz.time + 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 + end end def test_parse_with_day_omitted @@ -284,9 +286,10 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_missing_time_components zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - zone.stubs(:now).returns zone.local(1999, 12, 31, 12, 59, 59) - twz = zone.parse('2012-12-01') - assert_equal Time.utc(2012, 12, 1), twz.time + zone.stub(:now, zone.local(1999, 12, 31, 12, 59, 59)) do + twz = zone.parse('2012-12-01') + assert_equal Time.utc(2012, 12, 1), twz.time + end end def test_parse_with_javascript_date diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index 0e4e7427d2..55e8181b54 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -3,6 +3,7 @@ require 'active_support/xml_mini' require 'active_support/builder' require 'active_support/core_ext/hash' require 'active_support/core_ext/big_decimal' +require 'yaml' module XmlMiniTest class RenameKeyTest < ActiveSupport::TestCase diff --git a/ci/travis.rb b/ci/travis.rb index b62f90a59e..52fef05fbf 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -16,13 +16,14 @@ end class Build MAP = { 'railties' => 'railties', - 'ap' => 'actionpack', - 'am' => 'actionmailer', - 'amo' => 'activemodel', - 'as' => 'activesupport', - 'ar' => 'activerecord', - 'av' => 'actionview', - 'aj' => 'activejob' + 'ap' => 'actionpack', + 'am' => 'actionmailer', + 'amo' => 'activemodel', + 'as' => 'activesupport', + 'ar' => 'activerecord', + 'av' => 'actionview', + 'aj' => 'activejob', + 'guides' => 'guides' } attr_reader :component, :options @@ -36,7 +37,11 @@ class Build self.options.update(options) Dir.chdir(dir) do announce(heading) - rake(*tasks) + if guides? + run_bug_report_templates + else + rake(*tasks) + end end end @@ -71,6 +76,10 @@ class Build gem == 'activerecord' end + def guides? + gem == 'guides' + end + def isolated? options[:isolated] end @@ -96,6 +105,12 @@ class Build end true end + + def run_bug_report_templates + Dir.glob('bug_report_templates/*.rb').all? do |file| + system(Gem.ruby, '-w', file) + end + end end if ENV['GEM']=='aj:integration' @@ -110,6 +125,7 @@ ENV['GEM'].split(',').each do |gem| next if ENV['TRAVIS_PULL_REQUEST'] && ENV['TRAVIS_PULL_REQUEST'] != 'false' && isolated next if gem == 'railties' && isolated next if gem == 'aj:integration' && isolated + next if gem == 'guides' && isolated build = Build.new(gem, :isolated => isolated) results[build.key] = build.run! diff --git a/guides/Rakefile b/guides/Rakefile index 3c2099ac02..00577377d7 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -7,7 +7,7 @@ namespace :guides do desc "Generate HTML guides" task :html do - ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this + ENV["WARNINGS"] = "1" # authors can't disable this ruby "rails_guides.rb" end diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png Binary files differindex 3e07c948a0..4d0cb417b7 100644 --- a/guides/assets/images/getting_started/rails_welcome.png +++ b/guides/assets/images/getting_started/rails_welcome.png diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 032e6bfe11..58ba708a39 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -1,7 +1,16 @@ -# Activate the gem you are reporting the issue against. -gem 'rails', '4.2.0' +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' + # Activate the gem you are reporting the issue against. + gem 'rails', '4.2.0' +end -require 'rails' require 'rack/test' require 'action_controller/railtie' @@ -23,7 +32,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 9be8130884..1a4b736348 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -1,17 +1,16 @@ -unless File.exist?('Gemfile') - File.write('Gemfile', <<-GEMFILE) - source 'https://rubygems.org' - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - GEMFILE - - system 'bundle' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e end -require 'bundler' -Bundler.setup(:default) +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' +end -require 'rails' require 'action_controller/railtie' class TestApp < Rails::Application @@ -32,7 +31,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index b295d9d21f..09d6e7b331 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -1,5 +1,17 @@ -# Activate the gem you are reporting the issue against. -gem 'activerecord', '4.2.0' +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' + # Activate the gem you are reporting the issue against. + gem 'activerecord', '4.2.0' + gem 'sqlite3' +end + require 'active_record' require 'minitest/autorun' require 'logger' diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 9557f0b7c5..270dbe7df7 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -1,16 +1,16 @@ -unless File.exist?('Gemfile') - File.write('Gemfile', <<-GEMFILE) - source 'https://rubygems.org' - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - gem 'sqlite3' - GEMFILE - - system 'bundle' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e end -require 'bundler' -Bundler.setup(:default) +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' + gem 'sqlite3' +end require 'active_record' require 'minitest/autorun' diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index 3e3f8593d4..a4fe51156d 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -1,6 +1,16 @@ -# Activate the gems you are reporting the issue against. -gem 'activesupport', '4.2.0' -require 'active_support' +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' + # Activate the gem you are reporting the issue against. + gem 'activesupport', '4.2.0' +end + require 'active_support/core_ext/object/blank' require 'minitest/autorun' diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index d930482d4e..b6b4562751 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -1,15 +1,15 @@ -unless File.exist?('Gemfile') - File.write('Gemfile', <<-GEMFILE) - source 'https://rubygems.org' - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - GEMFILE - - system 'bundle' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e end -require 'bundler' -Bundler.setup(:default) +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' +end require 'active_support' require 'active_support/core_ext/object/blank' diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 9ad32e8168..696493a3cf 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -88,7 +88,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](http://github.com/carlhuda/bundler,) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 537aa5a371..327495704a 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -151,7 +151,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge @@ -199,7 +199,7 @@ Railties * jQuery is the new default JavaScript library. -* jQuery and Prototype are no longer vendored and is provided from now on by the jquery-rails and prototype-rails gems. +* jQuery and Prototype are no longer vendored and is provided from now on by the `jquery-rails` and `prototype-rails` gems. * The application generator accepts an option `-j` which can be an arbitrary string. If passed "foo", the gem "foo-rails" is added to the `Gemfile`, and the application JavaScript manifest requires "foo" and "foo_ujs". Currently only "prototype-rails" and "jquery-rails" exist and provide those files via the asset pipeline. diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index 6ddf77d9c0..c52c39b705 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -81,7 +81,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io/) ### Living on the Edge diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 9feaff098a..b9444510ea 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -36,7 +36,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io) ### Living on the Edge diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 7d95d4792e..09fbdc0d32 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -185,7 +185,9 @@ end These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed to `url_for` calls. -If you define `default_url_options` in `ApplicationController`, as in the example above, it will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. +If you define `default_url_options` in `ApplicationController`, as in the example above, these defaults will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. + +In a given request, the method is not actually called for every single generated URL; for performance reasons, the returned hash is cached, there is at most one invocation per request. ### Strong Parameters @@ -736,7 +738,7 @@ You can choose not to yield and build the response yourself, in which case the a While the most common way to use filters is by creating private methods and using *_action to add them, there are two other ways to do the same thing. -The first is to use a block directly with the *\_action methods. The block receives the controller as an argument, and the `require_login` filter from above could be rewritten to use a block: +The first is to use a block directly with the *\_action methods. The block receives the controller as an argument. The `require_login` filter from above could be rewritten to use a block: ```ruby class ApplicationController < ActionController::Base diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index bf3bf5d19e..c39cd34e9a 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -326,7 +326,7 @@ key. The list of emails can be an array of email addresses or a single string with the addresses separated by commas. ```ruby -class AdminMailer < ActionMailer::Base +class AdminMailer < ApplicationMailer default to: Proc.new { Admin.pluck(:email) }, from: 'notification@example.com' @@ -533,6 +533,24 @@ url helper. 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. +### Adding images in Action Mailer Views + +Unlike controllers, the mailer instance doesn't have any context about the +incoming request so you'll need to provide the `:asset_host` parameter yourself. + +As the `:asset_host` usually is consistent across the application you can +configure it globally in config/application.rb: + +```ruby +config.action_mailer.asset_host = 'http://example.com' +``` + +Now you can display an image inside your email. + +```ruby +<%= image_tag 'image.jpg' %> +``` + ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different @@ -696,7 +714,7 @@ files (environment.rb, production.rb, etc...) | Configuration | Description | |---------------|-------------| |`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.| -|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| +|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the constant (`OpenSSL::SSL::VERIFY_NONE`, `OpenSSL::SSL::VERIFY_PEER`, ...).</li></ul>| |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| @@ -741,6 +759,9 @@ config.action_mailer.smtp_settings = { authentication: 'plain', 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 +use another ESP to send email by replacing 'smpt.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 950bb5e358..db7eeed19a 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -15,7 +15,7 @@ After reading this guide, you will know: What is Action View? -------------------- -Action View and Action Controller are the two major components of Action Pack. In Rails, web requests are handled by Action Pack, which splits the work into a controller part (performing the logic) and a view part (rendering a template). Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. +In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves. @@ -214,7 +214,8 @@ By default `ActionView::Partials::PartialRenderer` has its object in a local var <%= render partial: "product" %> ``` -within product we'll get `@product` in the local variable `product`, as if we had written: +within `_product` partial we'll get `@product` in the local variable `product`, +as if we had written: ```erb <%= render partial: "product", locals: { product: @product } %> @@ -706,7 +707,7 @@ Returns a select tag with options for each of the minutes 0 through 59 with the ```ruby # Generates a select field for minutes that defaults to the minutes for the time provided. -select_minute(Time.now + 6.hours) +select_minute(Time.now + 10.minutes) ``` #### select_month @@ -724,7 +725,7 @@ Returns a select tag with options for each of the seconds 0 through 59 with the ```ruby # Generates a select field for seconds that defaults to the seconds for the time provided -select_second(Time.now + 16.minutes) +select_second(Time.now + 16.seconds) ``` #### select_time @@ -1058,14 +1059,6 @@ If `@article.author_ids` is [1], this would return: <input name="article[author_ids][]" type="hidden" value="" /> ``` -#### country_options_for_select - -Returns a string of option tags for pretty much any country in the world. - -#### country_select - -Returns select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. - #### option_groups_from_collection_for_select Returns a string of `option` tags, like `options_from_collection_for_select`, but groups them by `optgroup` tags based on the object relationships of the arguments. @@ -1152,8 +1145,8 @@ If `@article.person_id` is 1, this would become: <select name="article[person_id]"> <option value=""></option> <option value="1" selected="selected">David</option> - <option value="2">Sam</option> - <option value="3">Tobias</option> + <option value="2">Eileen</option> + <option value="3">Rafael</option> </select> ``` @@ -1428,7 +1421,7 @@ This sanitize helper will HTML encode all tags and strip all attributes that are sanitize @article.body ``` -If either the `:attributes` or `:tags` options are passed, only the mentioned tags and attributes are allowed and nothing else. +If either the `:attributes` or `:tags` options are passed, only the mentioned attributes and tags are allowed and nothing else. ```ruby sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 29d0c32b09..e3502d7363 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -4,7 +4,7 @@ Active Job Basics ================= This guide provides you with all you need to get started in creating, -enqueueing and executing background jobs. +enqueuing and executing background jobs. After reading this guide, you will know: @@ -20,7 +20,7 @@ Introduction ------------ Active Job is a framework for declaring jobs and making them run on a variety -of queueing backends. These jobs can be everything from regularly scheduled +of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really. @@ -28,11 +28,14 @@ into small units of work and run in parallel, really. The Purpose of Active Job ----------------------------- The main point is to ensure that all Rails apps will have a job infrastructure -in place, even if it's in the form of an "immediate runner". We can then have -framework features and other gems build on top of that, without having to -worry about API differences between various job runners such as Delayed Job -and Resque. Picking your queuing backend becomes more of an operational concern, -then. And you'll be able to switch between them without having to rewrite your jobs. +in place. We can then have framework features and other gems build on top of that, +without having to worry about API differences between various job runners such as +Delayed Job and Resque. Picking your queuing backend becomes more of an operational +concern, then. And you'll be able to switch between them without having to rewrite +your jobs. + +NOTE: Rails by default comes with an "immediate runner" queuing implementation. +That means that each job that has been enqueued will run immediately. Creating a Job @@ -67,49 +70,59 @@ Here's what a job looks like: class GuestsCleanupJob < ActiveJob::Base queue_as :default - def perform(*args) + def perform(*guests) # Do something later end end ``` +Note that you can define `perform` with as many arguments as you want. + ### Enqueue the Job Enqueue a job like so: ```ruby -# Enqueue a job to be performed as soon the queueing system is +# Enqueue a job to be performed as soon the queuing system is # free. -MyJob.perform_later record +GuestsCleanupJob.perform_later guest ``` ```ruby # Enqueue a job to be performed tomorrow at noon. -MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record) +GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) ``` ```ruby # Enqueue a job to be performed 1 week from now. -MyJob.set(wait: 1.week).perform_later(record) +GuestsCleanupJob.set(wait: 1.week).perform_later(guest) ``` -That's it! +```ruby +# `perform_now` and `perform_later` will call `perform` under the hood so +# you can pass as many arguments as defined in the latter. +GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') +``` +That's it! Job Execution ------------- -If no adapter is set, the job is immediately executed. +For enqueuing and executing jobs you need to set up a queuing backend, that is to +say you need to decide for a 3rd-party queuing library that Rails should use. +Rails itself does not provide a sophisticated queuing system and just executes the +job immediately if no adapter is set. ### Backends -Active Job has built-in adapters for multiple queueing backends (Sidekiq, +Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). ### Setting the Backend -You can easily set your queueing backend: +You can easily set your queuing backend: ```ruby # config/application.rb @@ -123,6 +136,10 @@ module YourApp end ``` +NOTE: Since jobs run in parallel to your Rails application, most queuing libraries +require that you start a library-specific queuing service (in addition to +starting your Rails app) for the job processing to work. For information on +how to do that refer to the documentation of your respective library. Queues ------ @@ -212,7 +229,7 @@ end ProcessVideoJob.perform_later(Video.last) ``` -NOTE: Make sure your queueing backend "listens" on your queue name. For some +NOTE: Make sure your queuing backend "listens" on your queue name. For some backends you need to specify the queues to listen to. @@ -270,6 +287,19 @@ UserMailer.welcome(@user).deliver_later ``` +Internationalization +-------------------- + +Each job uses the `I18n.locale` set when the job was created. Useful if you send +emails asynchronously: + +```ruby +I18n.locale = :eo + +UserMailer.welcome(@user).deliver_later # Email will be localized to Esparanto. +``` + + GlobalID -------- @@ -320,6 +350,13 @@ class GuestsCleanupJob < ActiveJob::Base end ``` +### Deserialization + +GlobalID allows serializing full Active Record objects passed to `#perform`. + +If a passed record is deleted after the job is enqueued but before the `#perform` +method is called Active Job will raise an `ActiveJob::DeserializationError` +exception. Job Testing -------------- diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 4b2bfaee2f..81e2a69504 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -8,12 +8,12 @@ classes. Active Model allows for Action Pack helpers to interact with plain Ruby objects. Active Model also helps build custom ORMs for use outside of the Rails framework. -After reading this guide, you will be able to add to plain Ruby objects: +After reading this guide, you will know: -* The ability to behave like an Active Record model. -* Callbacks and validations like Active Record. -* Serializers. -* Integration with the Rails internationalization (i18n) framework. +* How an Active Record model behaves. +* How Callbacks and validations work. +* How serializers work. +* The Rails internationalization (i18n) framework. -------------------------------------------------------------------------------- @@ -319,9 +319,8 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides two serializers `ActiveModel::Serializers::JSON` and -`ActiveModel::Serializers::Xml`. Both of these modules automatically include -the `ActiveModel::Serialization`. +Rails provides a `ActiveModel::Serializers::JSON` serializer. +This module automatically include the `ActiveModel::Serialization`. ##### ActiveModel::Serializers::JSON @@ -379,62 +378,6 @@ person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob"> person.name # => "Bob" ``` -##### ActiveModel::Serializers::Xml - -To use the `ActiveModel::Serializers::Xml` you only need to change from -`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`. - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes - {'name' => nil} - end -end -``` - -With the `to_xml` you have an XML representing the model. - -```ruby -person = Person.new -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n" -person.name = "Bob" -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n" -``` - -From an XML string you define the attributes of the model. -You need to have the `attributes=` method defined on your class: - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes=(hash) - hash.each do |key, value| - send("#{key}=", value) - end - end - - def attributes - {'name' => nil} - end -end -``` - -Now it is possible to create an instance of person and set the attributes using `from_xml`. - -```ruby -xml = { name: 'Bob' }.to_xml -person = Person.new -person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob"> -person.name # => "Bob" -``` - ### Translation `ActiveModel::Translation` provides integration between your object and the Rails diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index a227b54040..dafbe17bbd 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -142,7 +142,7 @@ end This will create a `Product` model, mapped to a `products` table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. Suppose -that the `products` table was created using an SQL sentence like: +that the `products` table was created using an SQL statement like: ```sql CREATE TABLE products ( @@ -260,7 +260,7 @@ david = User.find_by(name: 'David') ```ruby # find all users named David who are Code Artists and sort by created_at in reverse chronological order -users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC') +users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc) ``` You can learn more about querying an Active Record model in the [Active Record diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 80b1bde1c7..4e5902fb3d 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -357,8 +357,8 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table ### Creating a Join Table -Migration method `create_join_table` creates an HABTM join table. A typical use -would be: +The migration method `create_join_table` creates an HABTM (has and belongs to +many) join table. A typical use would be: ```ruby create_join_table :products, :categories @@ -367,23 +367,21 @@ create_join_table :products, :categories which creates a `categories_products` table with two columns called `category_id` and `product_id`. These columns have the option `:null` set to `false` by default. This can be overridden by specifying the `:column_options` -option. +option: ```ruby -create_join_table :products, :categories, column_options: {null: true} +create_join_table :products, :categories, column_options: { null: true } ``` -will create the `product_id` and `category_id` with the `:null` option as -`true`. - -You can pass the option `:table_name` when you want to customize the table -name. For example: +By default, the name of the join table comes from the union of the first two +arguments provided to create_join_table, in alphabetical order. +To customize the name of the table, provide a `:table_name` option: ```ruby create_join_table :products, :categories, table_name: :categorization ``` -will create a `categorization` table. +creates a `categorization` table. `create_join_table` also accepts a block, which you can use to add indices (which are not created by default) or additional columns: @@ -423,21 +421,23 @@ change_column :products, :part_number, :text ``` This changes the column `part_number` on products table to be a `:text` field. +Note that `change_column` command is irreversible. Besides `change_column`, the `change_column_null` and `change_column_default` -methods are used specifically to change a not null constraint and default values of a -column. +methods are used specifically to change a not null constraint and default +values of a column. ```ruby change_column_null :products, :name, false -change_column_default :products, :approved, false +change_column_default :products, :approved, from: true, to: false ``` This sets `:name` field on products to a `NOT NULL` column and the default -value of the `:approved` field to false. +value of the `:approved` field from true to false. -TIP: Unlike `change_column` (and `change_column_default`), `change_column_null` -is reversible. +Note: You could also write the above `change_column_default` migration as +`change_column_default :products, :approved, false`, but unlike the previous +example, this would make your migration irreversible. ### Column Modifiers @@ -475,7 +475,8 @@ column names can not be derived from the table names, you can use the `:column` and `:primary_key` options. Rails will generate a name for every foreign key starting with -`fk_rails_` followed by 10 random characters. +`fk_rails_` followed by 10 characters which are deterministically +generated from the `from_table` and `column`. There is a `:name` option to specify a different name if needed. NOTE: Active Record only supports single column foreign keys. `execute` and @@ -1003,7 +1004,10 @@ such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -Some people use migrations to add data to the 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 @@ -1019,9 +1023,11 @@ class AddInitialProducts < ActiveRecord::Migration end ``` -However, Rails has a 'seeds' feature that should be used for seeding a database -with initial data. It's a really simple feature: just fill up `db/seeds.rb` -with some Ruby code, and run `rake db:seed`: +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 `rake db:seed`: ```ruby 5.times do |i| diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index dcc523eb0f..9d495dfacb 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -252,7 +252,7 @@ extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| - t.column :identifier, :uuid + t.uuid :identifier end # app/models/revision.rb @@ -266,13 +266,14 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` -You can use `uuid` type to define references in migrations +You can use `uuid` type to define references in migrations: ```ruby # db/migrate/20150418012400_create_blog.rb -create_table :posts, id: :uuid +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :posts, id: :uuid, default: 'gen_random_uuid()' -create_table :comments, id: :uuid do |t| +create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t| # t.belongs_to :post, type: :uuid t.references :post, type: :uuid end @@ -288,6 +289,8 @@ class Comment < ActiveRecord::Base end ``` +See [this section](#uuid-primary-keys) for more details on using UUIDs as primary key. + ### Bit String Types * [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html) @@ -377,6 +380,9 @@ device = Device.create device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" ``` +NOTE: `uuid_generate_v4()` (from `uuid-ossp`) 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 e3cfabb327..4b4d70d3ce 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1266,6 +1266,18 @@ class Client < ActiveRecord::Base end ``` +NOTE: The `default_scope` is also applied while creating/building a record. +It is not applied while updating a record. E.g.: + +```ruby +class Client < ActiveRecord::Base + default_scope { where(active: true) } +end + +Client.new # => #<Client id: nil, active: true> +Client.unscoped.new # => #<Client id: nil, active: nil> +``` + ### Merging of scopes Just like `where` clauses scopes are merged using `AND` conditions. diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 7932853c11..71ca7a0f66 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -965,8 +965,9 @@ own custom validators. You can also create methods that verify the state of your models and add messages to the `errors` collection when they are invalid. You must then -register these methods by using the `validate` class method, passing in the -symbols for the validation methods' names. +register these methods by using the `validate` +([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate)) +class method, passing in the symbols for the validation methods' names. You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered. diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index e6475f2bb5..01bf928407 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1936,6 +1936,8 @@ as well as adding or subtracting their results from a Time object. For example: (4.months + 5.years).from_now ``` +NOTE: Defined in `active_support/core_ext/numeric/time.rb` + ### Formatting Enables the formatting of numbers in a variety of ways. @@ -2893,7 +2895,7 @@ The method `transform_values` accepts a block and returns a hash that has applie ``` There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver. -NOTE: Defined in `active_support/core_text/hash/transform_values.rb`. +NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`. ### Slicing diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 1b14bedfbf..cd44c685ba 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -244,13 +244,19 @@ INFO. The adapters will add their own data as well. } ``` -### identity.active_record +### instantiation.active_record | Key | Value | | ---------------- | ----------------------------------------- | -| `:line` | Primary Key of object in the identity map | -| `:name` | Record's class | -| `:connection_id` | `self.object_id` | +| `:record_count` | Number of records that instantiated | +| `:class_name` | Record's class | + +```ruby +{ + record_count: 1, + class_name: "User" +} +``` Action Mailer ------------- @@ -307,17 +313,6 @@ Action Mailer } ``` -Active Resource --------------- - -### request.active_resource - -| Key | Value | -| -------------- | -------------------- | -| `:method` | HTTP method | -| `:request_uri` | Complete URI | -| `:result` | HTTP response object | - Active Support -------------- diff --git a/guides/source/api_app.md b/guides/source/api_app.md new file mode 100644 index 0000000000..29ca872254 --- /dev/null +++ b/guides/source/api_app.md @@ -0,0 +1,408 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + + +Using Rails for API-only Applications +===================================== + +In this guide you will learn: + +* What Rails provides for API-only applications +* How to configure Rails to start without any browser features +* How to decide which middlewares you will want to include +* How to decide which modules to use in your controller + +-------------------------------------------------------------------------------- + +What is an API app? +------------------- + +Traditionally, when people said that they used Rails as an "API", they meant +providing a programmatically accessible API alongside their web application. +For example, GitHub provides [an API](http://developer.github.com) that you +can use from your own custom clients. + +With the advent of client-side frameworks, more developers are using Rails to +build a back-end that is shared between their web application and other native +applications. + +For example, Twitter uses its [public API](https://dev.twitter.com) in its web +application, which is built as a static site that consumes JSON resources. + +Instead of using Rails to generate dynamic HTML that will communicate with the +server through forms and links, many developers are treating their web application +as just another client, delivered as static HTML, CSS and JavaScript consuming +a simple JSON API. + +This guide covers building a Rails application that serves JSON resources to an +API client **or** a client-side framework. + +Why use Rails for JSON APIs? +---------------------------- + +The first question a lot of people have when thinking about building a JSON API +using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I +just use something like Sinatra?". + +For very simple APIs, this may be true. However, even in very HTML-heavy +applications, most of an application's logic is actually outside of the view +layer. + +The reason most people use Rails is that it provides a set of defaults that +allows us to get up and running quickly without having to make a lot of trivial +decisions. + +Let's take a look at some of the things that Rails provides out of the box that are +still applicable to API applications. + +Handled at the middleware layer: + +- Reloading: Rails applications support transparent reloading. This works even if + your application gets big and restarting the server for every request becomes + non-viable. +- Development Mode: Rails applications come with smart defaults for development, + making development pleasant without compromising production-time performance. +- Test Mode: Ditto development mode. +- Logging: Rails applications log every request, with a level of verbosity + appropriate for the current mode. Rails logs in development include information + about the request environment, database queries, and basic performance + information. +- Security: Rails detects and thwarts [IP spoofing + attacks](http://en.wikipedia.org/wiki/IP_address_spoofing) and handles + cryptographic signatures in a [timing + attack](http://en.wikipedia.org/wiki/Timing_attack) aware way. Don't know what + an IP spoofing attack or a timing attack is? Exactly. +- Parameter Parsing: Want to specify your parameters as JSON instead of as a + URL-encoded String? No problem. Rails will decode the JSON for you and make + it available in `params`. Want to use nested URL-encoded parameters? That + works too. +- Conditional GETs: Rails handles conditional `GET`, (`ETag` and `Last-Modified`), + processing request headers and returning the correct response headers and status + code. All you need to do is use the + [`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F) + check in your controller, and Rails will handle all of the HTTP details for you. +- Caching: If you use `dirty?` with public cache control, Rails will automatically + cache your responses. You can easily configure the cache store. +- HEAD requests: Rails will transparently convert `HEAD` requests into `GET` ones, + and return just the headers on the way out. This makes `HEAD` work reliably in + all Rails APIs. + +While you could obviously build these up in terms of existing Rack middlewares, +this list demonstrates that the default Rails middleware stack provides a lot +of value, even if you're "just generating JSON". + +Handled at the Action Pack layer: + +- Resourceful Routing: If you're building a RESTful JSON API, you want to be + using the Rails router. Clean and conventional mapping from HTTP to controllers + means not having to spend time thinking about how to model your API in terms + of HTTP. +- URL Generation: The flip side of routing is URL generation. A good API based + on HTTP includes URLs (see [the GitHub gist API](http://developer.github.com/v3/gists/) + for an example). +- Header and Redirection Responses: `head :no_content` and + `redirect_to user_url(current_user)` come in handy. Sure, you could manually + add the response headers, but why? +- Caching: Rails provides page, action and fragment caching. Fragment caching + is especially helpful when building up a nested JSON object. +- Basic, Digest and Token Authentication: Rails comes with out-of-the-box support + for three kinds of HTTP authentication. +- Instrumentation: Rails has an instrumentation API that will trigger registered + handlers for a variety of events, such as action processing, sending a file or + data, redirection, and database queries. The payload of each event comes with + relevant information (for the action processing event, the payload includes + the controller, action, parameters, request format, request method and the + request's full path). +- Generators: This may be passé for advanced Rails users, but it can be nice to + generate a resource and get your model, controller, test stubs, and routes + created for you in a single command. +- Plugins: Many third-party libraries come with support for Rails that reduce + or eliminate the cost of setting up and gluing together the library and the + web framework. This includes things like overriding default generators, adding + rake tasks, and honoring Rails choices (like the logger and cache back-end). + +Of course, the Rails boot process also glues together all registered components. +For example, the Rails boot process is what uses your `config/database.yml` file +when configuring Active Record. + +**The short version is**: you may not have thought about which parts of Rails +are still applicable even if you remove the view layer, but the answer turns out +to be "most of it". + +The Basic Configuration +----------------------- + +If you're building a Rails application that will be an API server first and +foremost, you can start with a more limited subset of Rails and add in features +as needed. + +You can generate a new api Rails app: + +```bash +$ rails new my_api --api +``` + +This will do three main things for you: + +- Configure your application to start with a more limited set of middlewares + than normal. Specifically, it will not include any middleware primarily useful + for browser applications (like cookies support) by default. +- Make `ApplicationController` inherit from `ActionController::API` instead of + `ActionController::Base`. As with middlewares, this will leave out any Action + Controller modules that provide functionalities primarily used by browser + applications. +- Configure the generators to skip generating views, helpers and assets when + you generate a new resource. + +If you want to take an existing application and make it an API one, read the +following steps. + +In `config/application.rb` add the following line at the top of the `Application` +class definition: + +```ruby +config.api_only = true +``` + +Finally, inside `app/controllers/application_controller.rb`, instead of: + +```ruby +class ApplicationController < ActionController::Base +end +``` + +do: + +```ruby +class ApplicationController < ActionController::API +end +``` + +Choosing Middlewares +-------------------- + +An API application comes with the following middlewares by default: + +- `Rack::Sendfile` +- `ActionDispatch::Static` +- `Rack::Lock` +- `ActiveSupport::Cache::Strategy::LocalCache::Middleware` +- `ActionDispatch::RequestId` +- `Rails::Rack::Logger` +- `Rack::Runtime` +- `ActionDispatch::ShowExceptions` +- `ActionDispatch::DebugExceptions` +- `ActionDispatch::RemoteIp` +- `ActionDispatch::Reloader` +- `ActionDispatch::Callbacks` +- `ActionDispatch::ParamsParser` +- `Rack::Head` +- `Rack::ConditionalGet` +- `Rack::ETag` + +See the [internal middlewares](rails_on_rack.html#internal-middleware-stack) +section of the Rack guide for further information on them. + +Other plugins, including Active Record, may add additional middlewares. In +general, these middlewares are agnostic to the type of application you are +building, and make sense in an API-only Rails application. + +You can get a list of all middlewares in your application via: + +```bash +$ rake middleware +``` + +### Using the Cache Middleware + +By default, Rails will add a middleware that provides a cache store based on +the configuration of your application (memcache by default). This means that +the built-in HTTP cache will rely on it. + +For instance, using the `stale?` method: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at) + render json: @post + end +end +``` + +The call to `stale?` will compare the `If-Modified-Since` header in the request +with `@post.updated_at`. If the header is newer than the last modified, this +action will return a "304 Not Modified" response. Otherwise, it will render the +response and include a `Last-Modified` header in it. + +Normally, this mechanism is used on a per-client basis. The cache middleware +allows us to share this caching mechanism across clients. We can enable +cross-client caching in the call to `stale?`: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at, public: true) + render json: @post + end +end +``` + +This means that the cache middleware will store off the `Last-Modified` value +for a URL in the Rails cache, and add an `If-Modified-Since` header to any +subsequent inbound requests for the same URL. + +Think of it as page caching using HTTP semantics. + +NOTE: This middleware is always outside of the `Rack::Lock` mutex, even in +single-threaded applications. + +### Using Rack::Sendfile + +When you use the `send_file` method inside a Rails controller, it sets the +`X-Sendfile` header. `Rack::Sendfile` is responsible for actually sending the +file. + +If your front-end server supports accelerated file sending, `Rack::Sendfile` +will offload the actual file sending work to the front-end server. + +You can configure the name of the header that your front-end server uses for +this purpose using `config.action_dispatch.x_sendfile_header` in the appropriate +environment's configuration file. + +You can learn more about how to use `Rack::Sendfile` with popular +front-ends in [the Rack::Sendfile +documentation](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile). + +Here are some values for popular servers, once they are configured, to support +accelerated file sending: + +```ruby +# Apache and lighttpd +config.action_dispatch.x_sendfile_header = "X-Sendfile" + +# Nginx +config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" +``` + +Make sure to configure your server to support these options following the +instructions in the `Rack::Sendfile` documentation. + +NOTE: The `Rack::Sendfile` middleware is always outside of the `Rack::Lock` +mutex, even in single-threaded applications. + +### Using ActionDispatch::ParamsParser + +`ActionDispatch::ParamsParser` will take parameters from the client in the JSON +format and make them available in your controller inside `params`. + +To use this, your client will need to make a request with JSON-encoded parameters +and specify the `Content-Type` as `application/json`. + +Here's an example in jQuery: + +```javascript +jQuery.ajax({ + type: 'POST', + url: '/people', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), + success: function(json) { } +}); +``` + +`ActionDispatch::ParamsParser` will see the `Content-Type` and your parameters +will be: + +```ruby +{ :person => { :firstName => "Yehuda", :lastName => "Katz" } } +``` + +### Other Middlewares + +Rails ships with a number of other middlewares that you might want to use in an +API application, especially if one of your API clients is the browser: + +- `Rack::MethodOverride` +- `ActionDispatch::Cookies` +- `ActionDispatch::Flash` +- For sessions management + * `ActionDispatch::Session::CacheStore` + * `ActionDispatch::Session::CookieStore` + * `ActionDispatch::Session::MemCacheStore` + +Any of these middlewares can be added via: + +```ruby +config.middleware.use Rack::MethodOverride +``` + +### Removing Middlewares + +If you don't want to use a middleware that is included by default in the API-only +middleware set, you can remove it with: + +```ruby +config.middleware.delete ::Rack::Sendfile +``` + +Keep in mind that removing these middlewares will remove support for certain +features in Action Controller. + +Choosing Controller Modules +--------------------------- + +An API application (using `ActionController::API`) comes with the following +controller modules by default: + +- `ActionController::UrlFor`: Makes `url_for` and friends available. +- `ActionController::Redirecting`: Support for `redirect_to`. +- `ActionController::Rendering`: Basic support for rendering. +- `ActionController::Renderers::All`: Support for `render :json` and friends. +- `ActionController::ConditionalGet`: Support for `stale?`. +- `ActionController::ForceSSL`: Support for `force_ssl`. +- `ActionController::RackDelegation`: Support for the `request` and `response` + methods returning `ActionDispatch::Request` and `ActionDispatch::Response` + objects. +- `ActionController::DataStreaming`: Support for `send_file` and `send_data`. +- `AbstractController::Callbacks`: Support for `before_filter` and friends. +- `ActionController::Instrumentation`: Support for the instrumentation + hooks defined by Action Controller (see [the instrumentation + guide](active_support_instrumentation.html#action-controller)). +- `ActionController::Rescue`: Support for `rescue_from`. +- `ActionController::BasicImplicitRender`: Makes sure to return an empty response + if there's not an explicit one. +- `ActionController::StrongParameters`: Support for parameters white-listing in + combination with Active Model mass assignment. +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash + so you don't have to specify root elements sending POST requests for instance. + +Other plugins may add additional modules. You can get a list of all modules +included into `ActionController::API` in the rails console: + +```bash +$ bin/rails c +>> ActionController::API.ancestors - ActionController::Metal.ancestors +``` + +### Adding Other Modules + +All Action Controller modules know about their dependent modules, so you can feel +free to include any modules into your controllers, and all dependencies will be +included and set up as well. + +Some common modules you might want to add: + +- `AbstractController::Translation`: Support for the `l` and `t` localization + and translation methods. +- `ActionController::HTTPAuthentication::Basic` (or `Digest` or `Token`): Support + for basic, digest or token HTTP authentication. +- `AbstractController::Layouts`: Support for layouts when rendering. +- `ActionController::MimeResponds`: Support for `respond_to`. +- `ActionController::Cookies`: Support for `cookies`, which includes + support for signed and encrypted cookies. This requires the cookies middleware. + +The best place to add a module is in your `ApplicationController` but you can +also add modules to individual controllers. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 4a610e8458..7b8d2d3aef 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -403,13 +403,13 @@ When using the asset pipeline, paths to assets must be re-written and underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet. -* `image-url("rails.png")` becomes `url(/assets/rails.png)` -* `image-path("rails.png")` becomes `"/assets/rails.png"`. +* `image-url("rails.png")` returns `url(/assets/rails.png)` +* `image-path("rails.png")` returns `"/assets/rails.png"` The more generic form can also be used: -* `asset-url("rails.png")` becomes `url(/assets/rails.png)` -* `asset-path("rails.png")` becomes `"/assets/rails.png"` +* `asset-url("rails.png")` returns `url(/assets/rails.png)` +* `asset-path("rails.png")` returns `"/assets/rails.png"` #### JavaScript/CoffeeScript and ERB diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 1fe111f2a0..1191f5edfe 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -435,7 +435,7 @@ end The simplest rule of thumb is that you should set up a `has_many :through` relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a `has_and_belongs_to_many` relationship (though you'll need to remember to create the joining table in the database). -You should use `has_many :through` if you need validations, callbacks, or extra attributes on the join model. +You should use `has_many :through` if you need validations, callbacks or extra attributes on the join model. ### Polymorphic Associations @@ -622,6 +622,19 @@ end We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit. +You can also use the method `create_join_table` + +```ruby +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration + def change + create_join_table :assemblies, :parts do |t| + t.index :assembly_id + t.index :part_id + end + end +end +``` + ### Controlling Association Scope By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example: @@ -1467,7 +1480,14 @@ The `collection_singular_ids=` method makes the collection contain only the obje ##### `collection.clear` -The `collection.clear` method removes every object from the collection. This destroys the associated objects if they are associated with `dependent: :destroy`, deletes them directly from the database if `dependent: :delete_all`, and otherwise sets their foreign keys to `NULL`. +The `collection.clear` method removes all objects from the collection according to the strategy specified by the `dependent` option. If no option is given, it follows the default strategy. The default strategy for `has_many :through` associations is `delete_all`, and for `has_many` associations is to set the foreign keys to `NULL`. + +```ruby +@customer.orders.clear +``` + +WARNING: Objects will be deleted if they're associated with `dependent: :destroy`, +just like `dependent: :delete_all`. ##### `collection.empty?` @@ -1506,26 +1526,36 @@ The `collection.where` method finds objects within the collection based on the c ##### `collection.exists?(...)` -The `collection.exists?` method checks whether an object meeting the supplied +The `collection.exists?` method checks whether an object meeting the supplied conditions exists in the collection. It uses the same syntax and options as [`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). ##### `collection.build(attributes = {}, ...)` -The `collection.build` method returns one or more new objects of the associated type. These objects will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. +The `collection.build` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. ```ruby @order = @customer.orders.build(order_date: Time.now, order_number: "A12345") + +@orders = @customer.orders.build([ + { order_date: Time.now, order_number: "A12346" }, + { order_date: Time.now, order_number: "A12347" } +]) ``` ##### `collection.create(attributes = {})` -The `collection.create` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. +The `collection.create` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. ```ruby @order = @customer.orders.create(order_date: Time.now, order_number: "A12345") + +@orders = @customer.orders.create([ + { order_date: Time.now, order_number: "A12346" }, + { order_date: Time.now, order_number: "A12347" } +]) ``` ##### `collection.create!(attributes = {})` @@ -2337,13 +2367,13 @@ associations, public methods, etc. Creating a car will save it in the `vehicles` table with "Car" as the `type` field: ```ruby -Car.create color: 'Red', price: 10000 +Car.create(color: 'Red', price: 10000) ``` will generate the following SQL: ```sql -INSERT INTO "vehicles" ("type", "color", "price") VALUES ("Car", "Red", 10000) +INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000) ``` Querying car records will just search for vehicles that are cars: diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 782406659d..9a56233e4a 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -1,14 +1,26 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -Caching with Rails: An overview +Caching with Rails: An Overview =============================== -This guide will teach you what you need to know about avoiding that expensive round-trip to your database and returning what you need to return to the web clients in the shortest time possible. +This guide is an introduction to speeding up your Rails application with caching. + +Caching means to store content generated during the request-response cycle and +to reuse it when responding to similar requests. + +Caching is often the most effective way to boost an application's performance. +Through caching, web sites running on a single server with a single database +can sustain a load of thousands of concurrent users. + +Rails provides a set of caching features out of the box. This guide will teach +you the scope and purpose of each one of them. Master these techniques and your +Rails applications can serve millions of views without exorbitant response times +or server bills. After reading this guide, you will know: -* Page and action caching (moved to separate gems as of Rails 4). -* Fragment caching. +* Fragment and Russian doll caching. +* How to manage the caching dependencies. * Alternative cache stores. * Conditional GET support. @@ -18,19 +30,32 @@ Basic Caching ------------- This is an introduction to three types of caching techniques: page, action and -fragment caching. Rails provides by default fragment caching. In order to use -page and action caching, you will need to add `actionpack-page_caching` and +fragment caching. By default Rails provides fragment caching. In order to use +page and action caching you will need to add `actionpack-page_caching` and `actionpack-action_caching` to your Gemfile. -To start playing with caching you'll want to ensure that `config.action_controller.perform_caching` is set to `true` if you're running in development mode. This flag is normally set in the corresponding `config/environments/*.rb` and caching is disabled by default for development and test, and enabled for production. +By default, caching is only enabled in your production environment. To play +around with caching locally you'll want to enable caching in your local +environment by setting `config.action_controller.perform_caching` to `true` in +the relevant `config/environments/*.rb` file: ```ruby config.action_controller.perform_caching = true ``` +NOTE: Changing the value of `config.action_controller.perform_caching` will +only have an effect on the caching provided by the Action Controller component. +For instance, it will not impact low-level caching, that we address +[below](#low-level-caching). + ### Page Caching -Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. +Page caching is a Rails mechanism which allows the request for a generated page +to be fulfilled by the webserver (i.e. Apache or NGINX) without having to go +through the entire Rails stack. While this is super fast it can't be applied to +every situation (such as pages that need authentication). Also, because the +webserver is serving a file directly from the filesystem you will need to +implement cache expiration. INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). @@ -42,105 +67,213 @@ INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_c ### Fragment Caching -Life would be perfect if we could get away with caching the entire contents of a page or action and serving it out to the world. Unfortunately, dynamic web applications usually build pages with a variety of components not all of which have the same caching characteristics. In order to address such a dynamically created page where different parts of the page need to be cached and expired differently, Rails provides a mechanism called Fragment Caching. +Dynamic web applications usually build pages with a variety of components not +all of which have the same caching characteristics. When different parts of the +page need to be cached and expired separately you can use Fragment Caching. Fragment Caching allows a fragment of view logic to be wrapped in a cache block and served out of the cache store when the next request comes in. -As an example, if you wanted to show all the orders placed on your website in real time and didn't want to cache that part of the page, but did want to cache the part of the page which lists all products available, you could use this piece of code: +For example, if you wanted to cache each product on a page, you could use this +code: ```html+erb -<% Order.find_recent.each do |o| %> - <%= o.buyer.name %> bought <%= o.product.name %> -<% end %> - -<% cache do %> - All available products: - <% Product.all.each do |p| %> - <%= link_to p.name, product_url(p) %> +<% @products.each do |product| %> + <% cache product do %> + <%= render product %> <% end %> <% end %> ``` -The cache block in our example will bind to the action that called it and is written out to the same place as the Action Cache, which means that if you want to cache multiple fragments per action, you should provide an `action_suffix` to the cache call: +When your application receives its first request to this page, Rails will write +a new cache entry with a unique key. A key looks something like this: -```html+erb -<% cache(action: 'recent', action_suffix: 'all_products') do %> - All available products: +``` +views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901 ``` -and you can expire it using the `expire_fragment` method, like so: +The number in the middle is the `product_id` followed by the timestamp value in +the `updated_at` attribute of the product record. Rails uses the timestamp value +to make sure it is not serving stale data. If the value of `updated_at` has +changed, a new key will be generated. Then Rails will write a new cache to that +key, and the old cache written to the old key will never be used again. This is +called key-based expiration. -```ruby -expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products') -``` +Cache fragments will also be expired when the view fragment changes (e.g., the +HTML in the view changes). The string of characters at the end of the key is a +template tree digest. It is an md5 hash computed based on the contents of the +view fragment you are caching. If you change the view fragment, the md5 hash +will change, expiring the existing file. -If you don't want the cache block to bind to the action that called it, you can also use globally keyed fragments by calling the `cache` method with a key: +TIP: Cache stores like Memcached will automatically delete old cache files. + +If you want to cache a fragment under certain conditions, you can use +`cache_if` or `cache_unless`: ```erb -<% cache('all_available_products') do %> - All available products: +<% cache_if admin?, product do %> + <%= render product %> <% end %> ``` -This fragment is then available to all actions in the `ProductsController` using the key and can be expired the same way: +#### Collection caching -```ruby -expire_fragment('all_available_products') -``` -If you want to avoid expiring the fragment manually, whenever an action updates a product, you can define a helper method: +The `render` helper can also cache individual templates rendered for a collection. +It can even one up the previous example with `each` by reading all cache +templates at once instead of one by one. This is done automatically if the template +rendered by the collection includes a `cache` call. Take a collection that renders +a `products/_product.html.erb` partial for each element: ```ruby -module ProductsHelper - def cache_key_for_products - count = Product.count - max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number) - "products/all-#{count}-#{max_updated_at}" - end -end +render products ``` -This method generates a cache key that depends on all products and can be used in the view: +If `products/_product.html.erb` starts with a `cache` call like so: -```erb -<% cache(cache_key_for_products) do %> - All available products: +```html+erb +<% cache product do %> + <%= product.name %> <% end %> ``` -If you want to cache a fragment under certain conditions, you can use `cache_if` or `cache_unless` +All the cached templates from previous renders will be fetched at once with much +greater speed. There's more info on how to make your templates [eligible for +collection caching](http://api.rubyonrails.org/classes/ActionView/Template/Handlers/ERB.html#method-i-resource_cache_call_pattern). + +### Russian Doll Caching + +You may want to nest cached fragments inside other cached fragments. This is +called Russian doll caching. + +The advantage of Russian doll caching is that if a single product is updated, +all the other inner fragments can be reused when regenerating the outer +fragment. + +As explained in the previous section, a cached file will expire if the value of +`updated_at` changes for a record on which the cached file directly depends. +However, this will not expire any cache the fragment is nested within. + +For example, take the following view: ```erb -<% cache_if (condition, cache_key_for_products) do %> - All available products: +<% cache product do %> + <%= render product.games %> <% end %> ``` -You can also use an Active Record model as the cache key: +Which in turn renders this view: ```erb -<% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(p) %> - <% end %> +<% cache game do %> + <%= render game %> <% end %> ``` -Behind the scenes, a method called `cache_key` will be invoked on the model and it returns a string like `products/23-20130109142513`. The cache key includes the model name, the id and finally the updated_at timestamp. Thus it will automatically generate a new fragment when the product is updated because the key changes. +If any attribute of game is changed, the `updated_at` value will be set to the +current time, thereby expiring the cache. However, because `updated_at` +will not be changed for the product object, that cache will not be expired and +your app will serve stale data. To fix this, we tie the models together with +the `touch` method: + +```ruby +class Product < ActiveRecord::Base + has_many :games +end + +class Game < ActiveRecord::Base + belongs_to :product, touch: true +end +``` + +With `touch` set to true, any action which changes `updated_at` for a game +record will also change it for the associated product, thereby expiring the +cache. -You can also combine the two schemes which is called "Russian Doll Caching": +### Managing dependencies -```erb -<% cache(cache_key_for_products) do %> - All available products: - <% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(p) %> - <% end %> - <% end %> +In order to correctly invalidate the cache, you need to properly define the +caching dependencies. Rails is clever enough to handle common cases so you don't +have to specify anything. However, sometimes, when you're dealing with custom +helpers for instance, you need to explicitly define them. + +#### Implicit dependencies + +Most template dependencies can be derived from calls to `render` in the template +itself. Here are some examples of render calls that `ActionView::Digestor` knows +how to decode: + +```ruby +render partial: "comments/comment", collection: commentable.comments +render "comments/comments" +render 'comments/comments' +render('comments/comments') + +render "header" => render("comments/header") + +render(@topic) => render("topics/topic") +render(topics) => render("topics/topic") +render(message.topics) => render("topics/topic") +``` + +On the other hand, some calls need to be changed to make caching work properly. +For instance, if you're passing a custom collection, you'll need to change: + +```ruby +render @project.documents.where(published: true) +``` + +to: + +```ruby +render partial: "documents/document", collection: @project.documents.where(published: true) +``` + +#### Explicit dependencies + +Sometimes you'll have template dependencies that can't be derived at all. This +is typically the case when rendering happens in helpers. Here's an example: + +```html+erb +<%= render_sortable_todolists @project.todolists %> +``` + +You'll need to use a special comment format to call those out: + +```html+erb +<%# Template Dependency: todolists/todolist %> +<%= render_sortable_todolists @project.todolists %> +``` + +In some cases, like a single table inheritance setup, you might have a bunch of +explicit dependencies. Instead of writing every template out, you can use a +wildcard to match any template in a directory: + +```html+erb +<%# Template Dependency: events/* %> +<%= render_categorizable_events @person.events %> +``` + +As for collection caching, if the partial template doesn't start with a clean +cache call, you can still benefit from collection caching by adding a special +comment format anywhere in the template, like: + +```html+erb +<%# Template Collection: notification %> +<% my_helper_that_calls_cache(some_arg, notification) do %> + <%= notification.name %> <% end %> ``` -It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment. +#### External dependencies + +If you use a helper method, for example, inside a cached block and you then update +that helper, you'll have to bump the cache as well. It doesn't really matter how +you do it, but the md5 of the template file must change. One recommendation is to +simply be explicit in a comment, like: + +```html+erb +<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> +<%= some_helper_method(person) %> +``` ### Low-Level Caching @@ -164,7 +297,10 @@ NOTE: Notice that in this example we used the `cache_key` method, so the resulti ### SQL Caching -Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again. +Query caching is a Rails feature that caches the result set returned by each +query. If Rails encounters the same query again for that request, it will use +the cached result set as opposed to running the query against the database +again. For example: @@ -186,21 +322,25 @@ end The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory. -However, it's important to note that query caches are created at the start of an action and destroyed at the end of that action and thus persist only for the duration of the action. If you'd like to store query results in a more persistent fashion, you can in Rails by using low level caching. +However, it's important to note that query caches are created at the start of +an action and destroyed at the end of that action and thus persist only for the +duration of the action. If you'd like to store query results in a more +persistent fashion, you can with low level caching. Cache Stores ------------ -Rails provides different stores for the cached data created by **action** and **fragment** caches. - -TIP: Page caches are always stored on disk. +Rails provides different stores for the cached data (apart from SQL and page +caching). ### Configuration -You can set up your application's default cache store by calling `config.cache_store=` in the Application definition inside your `config/application.rb` file or in an Application.configure block in an environment specific configuration file (i.e. `config/environments/*.rb`). The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor. +You can set up your application's default cache store by setting the +`config.cache_store` configuration option. Other parameters can be passed as +arguments to the cache store's constructor: ```ruby -config.cache_store = :memory_store +config.cache_store = :memory_store, { size: 64.megabytes } ``` NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block. @@ -225,15 +365,36 @@ There are some common options used by all cache implementations. These can be pa * `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option. +#### Custom Cache Stores + +You can create your own custom cache store by simply extending +`ActiveSupport::Cache::Store` and implementing the appropriate methods. This way, +you can swap in any number of caching technologies into your Rails application. + +To use a custom cache store, simply set the cache store to a new instance of your +custom class. + +```ruby +config.cache_store = MyCacheStore.new +``` + ### ActiveSupport::Cache::MemoryStore -This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` option to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. +This cache store keeps entries in memory in the same Ruby process. The cache +store has a bounded size specified by sending the `:size` option to the +initializer (default is 32Mb). When the cache exceeds the allotted size, a +cleanup will occur and the least recently used entries will be removed. ```ruby config.cache_store = :memory_store, { size: 64.megabytes } ``` -If you're running multiple Ruby on Rails server processes (which is the case if you're using mongrel_cluster or Phusion Passenger), then your Rails server process instances won't be able to share cache data with each other. This cache store is not appropriate for large application deployments, but can work well for small, low traffic sites with only a couple of server processes or for development and test environments. +If you're running multiple Ruby on Rails server processes (which is the case +if you're using mongrel_cluster or Phusion Passenger), then your Rails server +process instances won't be able to share cache data with each other. This cache +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. ### ActiveSupport::Cache::FileStore @@ -243,9 +404,13 @@ This cache store uses the file system to store entries. The path to the director config.cache_store = :file_store, "/path/to/cache/directory" ``` -With this cache store, multiple server processes on the same host can share a cache. Server processes running on different hosts could share a cache by using a shared file system, but that set up would not be ideal and is not recommended. The cache store is appropriate for low to medium traffic sites that are served off one or two hosts. +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 +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. -Note that the cache will grow until the disk is full unless you periodically clear out old entries. +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. @@ -253,7 +418,10 @@ This is the default cache store implementation. This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy. -When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites. +When initializing the cache, you need to specify the addresses for all +memcached servers in your cluster. If none are specified, it will assume +memcached is running on localhost on the default port, but this is not an ideal +setup for larger sites. The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. @@ -261,36 +429,6 @@ The `write` and `fetch` methods on this cache accept two additional options that config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" ``` -### ActiveSupport::Cache::EhcacheStore - -If you are using JRuby you can use Terracotta's Ehcache as the cache store for your application. Ehcache is an open source Java cache that also offers an enterprise version with increased scalability, management, and commercial support. You must first install the jruby-ehcache-rails3 gem (version 1.1.0 or later) to use this cache store. - -```ruby -config.cache_store = :ehcache_store -``` - -When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache). - -In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including: - -| Property | Argument Type | Description | -| --------------------------- | ------------------- | ----------------------------------------------------------- | -| elementEvictionData | ElementEvictionData | Sets this element's eviction data instance. | -| eternal | boolean | Sets whether the element is eternal. | -| timeToIdle, tti | int | Sets time to idle | -| timeToLive, ttl, expires_in | int | Sets time to Live | -| version | long | Sets the version attribute of the ElementAttributes object. | - -These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples: - -```ruby -Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds) -caches_action :index, expires_in: 60.seconds, unless_exist: true -``` - -For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) . -For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html) - ### ActiveSupport::Cache::NullStore This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. @@ -299,19 +437,13 @@ This cache store implementation is meant to be used only in development or test config.cache_store = :null_store ``` -### Custom Cache Stores - -You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application. - -To use a custom cache store, simply set the cache store to a new instance of the class. - -```ruby -config.cache_store = MyCacheStore.new -``` - -### Cache Keys +Cache Keys +---------- -The keys used in a cache can be any object that responds to either `:cache_key` or `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id. +The keys used in a cache can be any object that responds to either `cache_key` or +`to_param`. You can implement the `cache_key` method on your classes if you need +to generate custom keys. Active Record will generate keys based on the class name +and record id. You can use Hashes and Arrays of values as cache keys. @@ -320,7 +452,12 @@ You can use Hashes and Arrays of values as cache keys. Rails.cache.read(site: "mysite", owners: [owner_1, owner_2]) ``` -The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules. +The keys you use on `Rails.cache` will not be the same as those actually used with +the storage engine. They may be modified with a namespace or altered to fit +technology backend constraints. This means, for instance, that you can't save +values with `Rails.cache` and then try to pull them out with the `dalli` gem. +However, you also don't need to worry about exceeding the memcached size limit or +violating syntax rules. Conditional GET support ----------------------- @@ -383,3 +520,9 @@ class ProductsController < ApplicationController end end ``` + +References +---------- + +* [DHH's article on key-based expiration](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) +* [Ryan Bates' Railscast on cache digests](http://railscasts.com/episodes/387-cache-digests) diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 315d8c14b6..cd265331d6 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -64,10 +64,10 @@ $ cd commandsapp $ bin/rails server => Booting WEBrick => Rails 5.0.0 application starting in development on http://localhost:3000 -=> Call with -d to detach +=> Run `rails server -h` for more startup options => Ctrl-C to shutdown server [2013-08-07 02:00:01] INFO WEBrick 1.3.1 -[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0] +[2013-08-07 02:00:01] INFO ruby 2.2.2 (2015-06-27) [x86_64-darwin11.2.0] [2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000 ``` @@ -260,7 +260,13 @@ $ bin/rake db:migrate == CreateHighScores: migrated (0.0019s) ====================================== ``` -INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions about code. In unit testing, we take a little part of code, say a method of a model, and test its inputs and outputs. Unit tests are your friend. The sooner you make peace with the fact that your quality of life will drastically increase when you unit test your code, the better. Seriously. We'll make one in a moment. +INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions +about code. In unit testing, we take a little part of code, say a method of a model, +and test its inputs and outputs. Unit tests are your friend. The sooner you make +peace with the fact that your quality of life will drastically increase when you unit +test your code, the better. Seriously. Please visit +[the testing guide](http://guides.rubyonrails.org/testing.html) for an in-depth +look at unit testing. Let's see the interface Rails created for us. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 0e669ed597..df9704830e 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -88,8 +88,6 @@ application. Accepts a valid week day symbol (e.g. `:monday`). end ``` -* `config.dependency_loading` is a flag that allows you to disable constant autoloading setting it to false. It only has effect if `config.cache_classes` is true, which it is by default in production mode. - * `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. * `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is true. All namespaces in the list must respond to the `eager_load!` method. @@ -163,8 +161,6 @@ pipeline is enabled. It is set to true by default. * `config.assets.cache_store` defines the cache store that Sprockets will use. The default is the Rails file store. -* `config.assets.version` is an option string that is used in MD5 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. @@ -185,12 +181,13 @@ The full set of methods that can be used in this block are as follows: * `assets` allows to create assets on generating a scaffold. Defaults to `true`. * `force_plural` allows pluralized model names. Defaults to `false`. * `helper` defines whether or not to generate helpers. Defaults to `true`. -* `integration_tool` defines which integration tool to use. Defaults to `nil`. +* `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`. * `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`. * `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. -* `resource_route` defines whether inject resource route definition in routes or not. Defaults to `true`. +* `resource_route` defines whether a resource route definition should be generated + or not. Defaults to `true`. * `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`. * `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`. * `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`. @@ -329,7 +326,7 @@ The schema dumper adds one additional configuration option: * `config.action_controller.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself. -* `config.action_controller.perform_caching` configures whether the application should perform caching or not. Set to false in development mode, true in production. +* `config.action_controller.perform_caching` configures whether the application should perform the caching features provided by the Action Controller component or not. Set to false in development mode, true in production. * `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`. @@ -417,7 +414,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. `config.action_view` includes a small number of configuration settings: -* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Record. The default is +* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is ```ruby Proc.new do |html_tag, instance| @@ -454,6 +451,9 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations. +* `config.action_view.automatically_disable_submit_tag` determines whether + submit_tag should automatically disable on click, this defaults to true. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -519,6 +519,9 @@ There are a number of settings available on `config.action_mailer`: config.action_mailer.show_previews = false ``` +* `config.action_mailer.deliver_later_queue_name` specifies the queue name for + mailers. By default this is `mailers`. + ### Configuring Active Support There are a few configuration options available in Active Support: @@ -1084,22 +1087,22 @@ development: timeout: 5000 ``` -Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit. +Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. The database connection pool is initially empty. As demand for connections increases it will create them until it reaches the connection pool limit. -Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue. +Any one request will check out a connection the first time it requires access to the database. At the end of the request it will check the connection back in. This means that the additional connection slot will be available again for the next request in the queue. If you try to use more connections than are available, Active Record will block -and wait for a connection from the pool. When it cannot get connection, a timeout -error similar to given below will be thrown. +you and wait for a connection from the pool. If it cannot get a connection, a +timeout error similar to that given below will be thrown. ```ruby ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: ``` -If you get the above error, you might want to increase the size of connection -pool by incrementing the `pool` option in `database.yml` +If you get the above error, you might want to increase the size of the +connection pool by incrementing the `pool` option in `database.yml` -NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. +NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited number of connections. Custom configuration diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 3279c99c42..3b944f1274 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -28,7 +28,7 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge If you've found a problem in Ruby on Rails which is not a security risk, do a search on GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you are unable to find any open GitHub issues addressing the problem you found, your next step will be to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) -Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. +Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to reproduce the bug and figure out a fix. Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. @@ -61,7 +61,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 fixes already existing behavior that is misbehaving. Sometimes, +anything that causes incorrect behavior. Sometimes, the core team will have to make a judgement 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. @@ -295,7 +295,7 @@ You can run a single test through ruby. For instance: ```bash $ cd actionmailer -$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` The `-n` option allows you to run a single method instead of the whole @@ -334,7 +334,7 @@ will now run the four of them in turn. You can also run any single test separately: ```bash -$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb +$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb ``` To run a single test against all adapters, use: diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 61ea0b44ef..1d995581fa 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -28,7 +28,7 @@ Ruby on Rails Guides: Credits <h3 class="section">Rails Guides Authors</h3> <%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as the Community Manager at <a href="http://spreecommerce.com">Spree Commerce</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. + Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. <% end %> <%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 96bf532868..c486009741 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -346,22 +346,11 @@ by asking the debugger for help. Type: `help` ``` (byebug) help -byebug 2.7.0 + h[elp][ <cmd>[ <subcmd>]] -Type 'help <command-name>' for help on a specific command - -Available commands: -backtrace delete enable help list pry next restart source up -break disable eval info method ps save step var -catch display exit interrupt next putl set thread -condition down finish irb p quit show trace -continue edit frame kill pp reload skip undisplay -``` - -TIP: To view the help menu for any command use `help <command-name>` at the -debugger prompt. For example: _`help list`_. You can abbreviate any debugging -command by supplying just enough letters to distinguish them from other -commands. For example, you can use `l` for the `list` command. + help -- prints this help. + help <cmd> -- prints help on command <cmd>. + help <cmd> <subcmd> -- prints help on <cmd>'s subcommand <subcmd>. To see the previous ten lines you should type `list-` (or `l-`). @@ -502,7 +491,7 @@ current context: (byebug) instance_variables [:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name, + :@_response, :@_prefixes, :@_lookup_context, :@_action_name, :@_response_body, :@marked_for_same_origin_verification, :@_config] ``` @@ -531,8 +520,11 @@ command later in this guide). And then ask again for the instance_variables: ``` -(byebug) instance_variables.include? "@articles" -true +(byebug) instance_variables +[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, + :@_response, :@_prefixes, :@_lookup_context, :@_action_name, + :@_response_body, :@marked_for_same_origin_verification, :@_config, + :@articles] ``` Now `@articles` is included in the instance variables, because the line defining it @@ -770,8 +762,8 @@ environment variable. A specific _line_ can also be given. ### Quitting -To exit the debugger, use the `quit` command (abbreviated `q`), or its alias -`exit`. +To exit the debugger, use the `quit` command (abbreviated to `q`). Or, type `q!` +to bypass the `Really quit? (y/n)` prompt and exit unconditionally. A simple quit tries to terminate all threads in effect. Therefore your server will be stopped and you will have to start it again. diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 989b29956c..3c670a1221 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -9,7 +9,7 @@ After reading this guide, you will know: * How to set up your machine for Rails development * How to run specific groups of unit tests from the Rails test suite -* How the ActiveRecord portion of the Rails test suite operates +* How the Active Record portion of the Rails test suite operates -------------------------------------------------------------------------------- @@ -21,14 +21,14 @@ The easiest and recommended way to get a development environment ready to hack i The Hard Way ------------ -In case you can't use the Rails development box, see section above, these are the steps to manually build a development box for Ruby on Rails core development. +In case you can't use the Rails development box, see section below, these are the steps to manually build a development box for Ruby on Rails core development. ### Install Git Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git: * [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics. -* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git +* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git. * [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by. * The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow. * [GitHub](http://help.github.com) offers links to a variety of Git resources. @@ -60,7 +60,7 @@ In Ubuntu you're done with just: $ sudo apt-get install sqlite3 libsqlite3-dev ``` -And if you are on Fedora or CentOS, you're done with +If you are on Fedora or CentOS, you're done with ```bash $ sudo yum install sqlite3 sqlite3-devel @@ -213,7 +213,7 @@ FreeBSD users will have to run the following: ```bash # pkg install mysql56-client mysql56-server -# pkg install postgresql93-client postgresql93-server +# pkg install postgresql94-client postgresql94-server ``` Or install them through ports (they are located under the `databases` folder). diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 7ae3640937..4473eba478 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -11,7 +11,7 @@ - name: Active Record Basics url: active_record_basics.html - description: This guide will get you started with models, persistence to database and the Active Record pattern and library. + description: This guide will get you started with models, persistence to database, and the Active Record pattern and library. - name: Active Record Migrations url: active_record_migrations.html @@ -19,7 +19,7 @@ - name: Active Record Validations url: active_record_validations.html - description: This guide covers how you can use Active Record validations + description: This guide covers how you can use Active Record validations. - name: Active Record Callbacks url: active_record_callbacks.html @@ -74,7 +74,7 @@ - name: Rails Internationalization API url: i18n.html - description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on. + description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on. - name: Action Mailer Basics url: action_mailer_basics.html @@ -82,7 +82,7 @@ - name: Active Job Basics url: active_job_basics.html - description: This guide provides you with all you need to get started in creating, enqueueing and executing background jobs. + description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs. - name: Testing Rails Applications work_in_progress: true @@ -116,12 +116,16 @@ name: The Rails Initialization Process work_in_progress: true url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 4 + description: This guide explains the internals of the Rails initialization process as of Rails 4. - name: Autoloading and Reloading Constants url: autoloading_and_reloading_constants.html description: This guide documents how autoloading and reloading constants work. - + name: "Caching with Rails: An Overview" + url: caching_with_rails.html + description: This guide is an introduction to speeding up your Rails application with caching. + - name: Active Support Instrumentation work_in_progress: true url: active_support_instrumentation.html diff --git a/guides/source/engines.md b/guides/source/engines.md index a89ed1984f..3b1588b75a 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -689,8 +689,8 @@ haven't been copied over already. The first run for this command will output something such as this: ```bash -Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh -Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh +Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh +Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh ``` The first timestamp (`[timestamp_1]`) will be the current time, and the second @@ -822,9 +822,9 @@ Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. ``` -NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been skipped. Migration with the same name already exists. -NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists. -Copied migration [timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh ``` Run the migration using: diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 853227e2a1..84a8d695cb 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -211,9 +211,8 @@ IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local, month, week, URL, email, number and range inputs are HTML5 controls. If you require your app to have a consistent experience in older browsers, you will need an HTML5 polyfill (provided by CSS and/or JavaScript). -There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are -[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/), -which provide a simple way to add functionality based on the presence of +There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a popular tool at the moment is +[Modernizr](http://www.modernizr.com/), which provides a simple way to add functionality based on the presence of detected HTML5 features. TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging). @@ -291,8 +290,8 @@ You can create a similar binding without actually creating `<form>` tags with th ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_details_form| %> - <%= contact_details_form.text_field :phone_number %> + <%= fields_for @person.contact_detail do |contact_detail_form| %> + <%= contact_detail_form.text_field :phone_number %> <% end %> <% end %> ``` @@ -441,7 +440,7 @@ Whenever Rails sees that the internal value of an option being generated matches 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. +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: diff --git a/guides/source/generators.md b/guides/source/generators.md index 14f451cbc9..32bbdc554a 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -503,6 +503,14 @@ Adds a specified source to `Gemfile`: add_source "http://gems.github.com" ``` +This method also takes a block: + +```ruby +add_source "http://gems.github.com" do + gem "rspec-rails" +end +``` + ### `inject_into_file` Injects a block of code into a defined position in your file. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 5ef376531d..ef66b75ec1 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -24,8 +24,11 @@ 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. -* The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby - versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org). +* 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). Rails is a web application framework running on the Ruby programming language. @@ -34,7 +37,6 @@ curve diving straight into Rails. There are several curated lists of online reso for learning Ruby: * [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/) -* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby) Be aware that some resources, while still excellent, cover versions of Ruby as old as 1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day @@ -89,17 +91,17 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: +```bash +$ ruby -v +ruby 2.2.2p95 +``` + 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). For more installation methods for most Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). -```bash -$ ruby -v -ruby 2.2.2p95 -``` - 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 @@ -163,7 +165,7 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | |app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.| +|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.| |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| @@ -298,6 +300,7 @@ Rails.application.routes.draw do # The priority is based upon order of creation: # first created -> highest priority. + # See how all your routes lay out with "rake routes". # # You can have the root of your site routed with "root" # root 'welcome#index' @@ -400,7 +403,7 @@ a controller called `ArticlesController`. You can do this by running this command: ```bash -$ bin/rails g controller articles +$ bin/rails generate controller articles ``` If you open up the newly generated `app/controllers/articles_controller.rb` @@ -619,7 +622,7 @@ def create end ``` -The `render` method here is taking a very simple hash with a key of `plain` and +The `render` method here is taking a very simple hash with a key of `:plain` and value of `params[:article].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns an `ActiveSupport::HashWithIndifferentAccess` object, which @@ -1240,7 +1243,9 @@ article we want to show the form back to the user. We reuse the `article_params` method that we defined earlier for the create action. -TIP: It is not necessary to pass all the attributes to `update`. For example, if `@article.update(title: 'A new title')` were called, Rails would only update the `title` attribute, leaving all other attributes untouched. +TIP: It is not necessary to pass all the attributes to `update`. For example, +if `@article.update(title: 'A new title')` was called, Rails would only update +the `title` attribute, leaving all other attributes untouched. Finally, we want to show a link to the `edit` action in the list of all the articles, so let's add that now to `app/views/articles/index.html.erb` to make @@ -1543,20 +1548,17 @@ class CreateComments < ActiveRecord::Migration create_table :comments do |t| t.string :commenter t.text :body - - # this line adds an integer column called `article_id`. - t.references :article, index: true + t.references :article, index: true, foreign_key: true t.timestamps null: false end - add_foreign_key :comments, :articles end end ``` -The `t.references` line sets up a foreign key column for the association between -the two models. An index for this association is also created on this column. -Go ahead and run the migration: +The `t.references` line creates an integer column called `article_id`, an index +for it, and a foreign key constraint that points to the `id` column of the `articles` +table. Go ahead and run the migration: ```bash $ bin/rake db:migrate @@ -1569,8 +1571,6 @@ run against the current database, so in this case you will just see: == CreateComments: migrating ================================================= -- create_table(:comments) -> 0.0115s --- add_foreign_key(:comments, :articles) - -> 0.0000s == CreateComments: migrated (0.0119s) ======================================== ``` diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 51eaf4ba5a..272a0e3623 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -210,14 +210,14 @@ This approach has almost the same set of advantages as setting the locale from t Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL, e.g. `link_to(books_url(locale: I18n.locale))`, would be tedious and probably impossible, of course. -Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding this method). +Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding `default_url_options`). We can include something like this in our `ApplicationController` then: ```ruby # app/controllers/application_controller.rb -def default_url_options(options = {}) - { locale: I18n.locale }.merge options +def default_url_options + { locale: I18n.locale } end ``` @@ -225,7 +225,7 @@ Every helper method dependent on `url_for` (e.g. helpers for named routes like ` You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this. -You probably want URLs to look like this: `www.example.com/en/books` (which loads the English locale) and `www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scoping`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html) option in this way: +You probably want URLs to look like this: `http://www.example.com/en/books` (which loads the English locale) and `http://www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scope`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html): ```ruby # config/routes.rb @@ -234,7 +234,9 @@ scope "/:locale" do end ``` -Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). An URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). +Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). A URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). + +WARNING. Since the return value of `default_url_options` is cached per request, the URLs in a locale selector cannot be generated invoking helpers in a loop that sets the corresponding `I18n.locale` in each iteration. Instead, leave `I18n.locale` untouched, and pass an explicit `:locale` option to the helper, or edit `request.original_fullpath`. If you don't want to force the use of a locale in your routes you can use an optional path scope (denoted by the parentheses) like so: @@ -500,7 +502,10 @@ You can make use of this feature, e.g. when working with a large amount of stati ### Organization of Locale Files -When you are using the default SimpleStore shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you. +When you are using the default SimpleStore shipped with the i18n library, +dictionaries are stored in plain-text files on the disk. Putting translations +for all parts of your application in one file per locale could be hard to +manage. You can store these files in a hierarchy which makes sense to you. For example, your `config/locales` directory could look like this: diff --git a/guides/source/kindle/welcome.html.erb b/guides/source/kindle/welcome.html.erb index 610a71570f..ef3397f58f 100644 --- a/guides/source/kindle/welcome.html.erb +++ b/guides/source/kindle/welcome.html.erb @@ -2,4 +2,6 @@ <h3>Kindle Edition</h3> -The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +<div> + The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +</div> diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index b3e1874048..b7364536c3 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -215,7 +215,7 @@ CODE ### yes?(question) or no?(question) -These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to freeze rails only if the user wants to: +These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to Freeze Rails only if the user wants to: ```ruby rake("rails:freeze:gems") if yes?("Freeze rails gems?") diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 993cd5ac44..1e2fe94010 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -58,23 +58,6 @@ class Server < ::Rack::Server end ``` -Here's how it loads the middlewares: - -```ruby -def middleware - middlewares = [] - middlewares << [::Rack::ContentLength] - Hash.new(middlewares) -end -``` - -`Rails::Rack::Debugger` is primarily useful only in the development environment. The following table explains the usage of the loaded middlewares: - -| Middleware | Purpose | -| ----------------------- | --------------------------------------------------------------------------------- | -| `Rails::Rack::Debugger` | Starts Debugger | -| `Rack::ContentLength` | Counts the number of bytes in the response and set the HTTP Content-Length header | - ### `rackup` To use `rackup` instead of Rails' `rails server`, you can put the following inside `config.ru` of your Rails application's root directory: @@ -82,9 +65,6 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi ```ruby # Rails.root/config.ru require ::File.expand_path('../config/environment', __FILE__) - -use Rails::Rack::Debugger -use Rack::ContentLength run Rails.application ``` @@ -329,8 +309,6 @@ Resources * [Official Rack Website](http://rack.github.io) * [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html) -* [Ruby on Rack #1 - Hello Rack!](http://m.onkey.org/ruby-on-rack-1-hello-rack) -* [Ruby on Rack #2 - The Builder](http://m.onkey.org/ruby-on-rack-2-the-builder) ### Understanding Middlewares diff --git a/guides/source/routing.md b/guides/source/routing.md index b1e4c8ad86..e4799d93fa 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -83,7 +83,9 @@ Rails would dispatch that request to the `destroy` method on the `photos` contro ### CRUD, Verbs, and Actions -In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as: +In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to +controller actions. By convention, each action also maps to a specific CRUD +operation in a database. A single entry in the routing file, such as: ```ruby resources :photos @@ -615,6 +617,8 @@ 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"`. +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 You can specify a name for any route using the `:as` option: @@ -793,7 +797,11 @@ get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` -Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. +Please note that default redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. You can use the `:status` option to change the response status: + +```ruby +get '/stories/:name', to: redirect('/articles/%{name}', status: 302) +``` In all of these cases, if you don't provide the leading host (`http://www.example.com`), Rails will take those details from the current request. @@ -809,13 +817,16 @@ As long as `Sprockets` responds to `call` and returns a `[status, headers, body] NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. -If you specify a rack application as the endpoint for a matcher remember that the route will be unchanged in the receiving application. With the following route your rack application should expect the route to be '/admin': +If you specify a Rack application as the endpoint for a matcher, remember that +the route will be unchanged in the receiving application. With the following +route your Rack application should expect the route to be '/admin': ```ruby match '/admin', to: AdminApp, via: :all ``` -If you would prefer to have your rack application receive requests at the root path instead use mount: +If you would prefer to have your Rack application receive requests at the root +path instead, use mount: ```ruby mount AdminApp, at: '/admin' @@ -1084,6 +1095,20 @@ edit_videos GET /videos/:identifier/edit(.:format) videos#edit Video.find_by(identifier: params[:identifier]) ``` +You can override `ActiveRecord::Base#to_param` of a related model to construct +an URL: + +```ruby +class Video < ActiveRecord::Base + def to_param + identifier + end +end + +video = Video.find_by(identifier: "Roman-Holiday") +edit_videos_path(video) # => "/videos/Roman-Holiday" +``` + Inspecting and Testing Routes ----------------------------- diff --git a/guides/source/security.md b/guides/source/security.md index 93580d4d4e..21cf48c2cf 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -93,9 +93,16 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves * Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_. -* 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 and inserted into the end of the cookie. +* 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. -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_. +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`. + +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 `rake secret` instead_. `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.: @@ -191,11 +198,10 @@ This attack method works by including malicious code or a link in a page that ac In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: -* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file. -* `<img src="http://www.webapp.com/project/1/destroy">` -* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago. -* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id. -* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. +* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file: `<img src="http://www.webapp.com/project/1/destroy">` +* Bob's session at `www.webapp.com` is still alive, because he didn't log out a few minutes ago. +* By viewing the post, the browser finds an image tag. It tries to load the suspected image from `www.webapp.com`. As explained before, it will also send along the cookie with the valid session id. +* The web application at `www.webapp.com` verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. * Bob doesn't notice the attack - but a few days later he finds out that project number one is gone. It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email. @@ -220,7 +226,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST ( 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. -_POST requests can be sent automatically, too_. Here is an example for a link which displays www.harmless.com as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request. +_POST requests can be sent automatically, too_. Here is an example for a link which displays `www.harmless.com` as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request. ```html <a href="http://www.harmless.com/" onclick=" @@ -1026,6 +1032,29 @@ Environmental Security It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. +### Custom secrets + +Rails generates a `config/secrets.yml`. By default, this file contains the +application's `secret_key_base`, but it could also be used to store other +secrets such as access keys for external APIs. + +The secrets added to this file are accessible via `Rails.application.secrets`. +For example, with the following `config/secrets.yml`: + + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY + +`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development +environment. + +If you want an exception to be raised when some key is blank, use the bang +version: + +```ruby +Rails.application.secrets.some_api_key! # => raises KeyError +``` + Additional Resources -------------------- diff --git a/guides/source/testing.md b/guides/source/testing.md index 2067fdb383..3a691220cf 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -25,15 +25,7 @@ Rails tests can also simulate browser requests and thus you can test your applic Introduction to Testing ----------------------- -Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data. - -### The Test Environment - -By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`. - -A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases. - -Also, each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`. +Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. ### Rails Sets up for Testing from the Word Go @@ -51,116 +43,18 @@ Fixtures are a way of organizing test data; they reside in the `fixtures` direct The `test_helper.rb` file holds the default configuration for your tests. -### The Low-Down on Fixtures - -For good tests, you'll need to give some thought to setting up test data. -In Rails, you can handle this by defining and customizing fixtures. -You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). - -#### What Are Fixtures? - -_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model. - -You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory. - -#### YAML - -YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). - -Here's a sample YAML fixture file: - -```yaml -# lo & behold! I am a YAML comment! -david: - name: David Heinemeier Hansson - birthday: 1979-10-15 - profession: Systems development - -steve: - name: Steve Ross Kellock - birthday: 1974-09-27 - profession: guy with keyboard -``` - -Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column. - -If you are working with [associations](/association_basics.html), you can simply -define a reference node between two different fixtures. Here's an example with -a `belongs_to`/`has_many` association: - -```yaml -# In fixtures/categories.yml -about: - name: About - -# In fixtures/articles.yml -one: - title: Welcome to Rails! - body: Hello world! - category: about -``` - -Notice the `category` key of the `one` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`. - -NOTE: For associations to reference one another by name, you cannot specify the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). - -#### ERB'in It Up - -ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: - -```erb -<% 1000.times do |n| %> -user_<%= n %>: - username: <%= "user#{n}" %> - email: <%= "user#{n}@example.com" %> -<% end %> -``` -#### Fixtures in Action - -Rails by default automatically loads all fixtures from the `test/fixtures` directory for your models and controllers test. Loading involves three steps: - -1. Remove any existing data from the table corresponding to the fixture -2. Load the fixture data into the table -3. Dump the fixture data into a method in case you want to access it directly - -TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)) - -#### Fixtures are Active Record objects - -Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example: - -```ruby -# this will return the User object for the fixture named david -users(:david) - -# this will return the property for david called id -users(:david).id +### The Test Environment -# one can also access methods available on the User class -email(david.partner.email, david.location_tonight) -``` +By default, every Rails application has three environments: development, test, and production. -### Console Tasks for Running your Tests +Each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`. -Rails comes with a CLI command to run tests. -Here are some examples of how to use it: +NOTE: Your test are run under RAILS_ENV=test. -```bash -$ bin/rails test # run all tests in the `test` directory -$ bin/rails test test/controllers # run all tests from specific directory -$ bin/rails test test/models/post_test.rb # run specific test -$ bin/rails test test/models/post_test.rb:44 # run specific test and line -``` - -We will cover each of types Rails tests listed above in this guide. - -Model Testing ------------------------- +### Rails meets Minitest -For this guide we will be using the application we built in the [Getting Started with Rails](getting_started.html) guide. - -If you remember when you used the `rails generate scaffold` command from earlier. We created our first resource among other things it created a test stub in the `test/models` directory: +If you remember when you used the `rails generate scaffold` command from the [Getting Started with Rails](getting_started.html) guide. We created our first resource among other things it created test stubs in the `test` directory: ```bash $ bin/rails generate scaffold article title:string body:text @@ -235,47 +129,6 @@ An assertion is a line of code that evaluates an object (or expression) for expe Every test must contain at least one assertion, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass. -### Maintaining the test database schema - -In order to run your tests, your test database will need to have the current -structure. The test helper checks whether your test database has any pending -migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` -into the test database. If migrations are still pending, an error will be -raised. Usually this indicates that your schema is not fully migrated. Running -the migrations against the development database (`bin/rake db:migrate`) will -bring the schema up to date. - -NOTE: If existing migrations required modifications, the test database needs to -be rebuilt. This can be done by executing `bin/rake db:test:prepare`. - -### Running Tests - -Running a test is as simple as invoking the file containing the test cases through `rails test` command. - -```bash -$ bin/rails test test/models/article_test.rb -. - -Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. - -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips -``` - -This will run all test methods from the test case. - -You can also run a particular test method from the test case by running the test and providing the `test method name`. - -```bash -$ bin/rails test test/models/article_test.rb test_the_truth -. - -Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. - -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips -``` - -The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. - #### Your first failing test To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case. @@ -428,8 +281,8 @@ specify to make your test failure messages clearer. It's not required. | `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.| | `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.| | `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.| -| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| -| `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| +| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| +| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| | `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| | `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.| @@ -467,11 +320,10 @@ Rails adds some custom assertions of its own to the `minitest` framework: | `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | 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)` | 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.| | `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.| -| `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.| You'll see the usage of some of these assertions in the next chapter. -### A Brief Note About Minitest +### A Brief Note About Test Cases All the basic assertions such as `assert_equal` defined in `Minitest::Assertions` are also available in the classes we use in our own test cases. In fact, Rails provides the following classes for you to inherit from: @@ -486,6 +338,297 @@ Each of these classes include `Minitest::Assertions`, allowing us to use all of NOTE: For more information on `Minitest`, refer to [Minitest](http://docs.seattlerb.org/minitest) +### The Rails Test Runner + +We can run all of our tests at once by using the `rails test` command. + +Or we can run a single test by passing the `rails test` command the filename containing the test cases. + +```bash +$ bin/rails test test/models/article_test.rb +. + +Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` + +This will run all test methods from the test case. + +You can also run a particular test method from the test case by providing the `-n` or `--name` flag and the `test method name`. + +```bash +$ bin/rails test test/models/article_test.rb -n test_the_truth +. + +Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` + +You can also run a test at a specific line by providing the line number. + +```bash +$ bin/rails test test/models/post_test.rb:44 # run specific test and line +``` + +You can also run an entire directory of tests by providing the path to the directory. + +```bash +$ bin/rails test test/controllers # run all tests from specific directory +``` + + +The Test Database +----------------- + +Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data. + +By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`. + +A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases. + + +### Maintaining the test database schema + +In order to run your tests, your test database will need to have the current +structure. The test helper checks whether your test database has any pending +migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` +into the test database. If migrations are still pending, an error will be +raised. Usually this indicates that your schema is not fully migrated. Running +the migrations against the development database (`bin/rake db:migrate`) will +bring the schema up to date. + +NOTE: If existing migrations required modifications, the test database needs to +be rebuilt. This can be done by executing `bin/rake db:test:prepare`. + +### The Low-Down on Fixtures + +For good tests, you'll need to give some thought to setting up test data. +In Rails, you can handle this by defining and customizing fixtures. +You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + +#### What Are Fixtures? + +_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model. + +You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory. + +#### YAML + +YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). + +Here's a sample YAML fixture file: + +```yaml +# lo & behold! I am a YAML comment! +david: + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development + +steve: + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard +``` + +Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column. + +If you are working with [associations](/association_basics.html), you can simply +define a reference node between two different fixtures. Here's an example with +a `belongs_to`/`has_many` association: + +```yaml +# In fixtures/categories.yml +about: + name: About + +# In fixtures/articles.yml +one: + title: Welcome to Rails! + body: Hello world! + category: about +``` + +Notice the `category` key of the `one` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`. + +NOTE: For associations to reference one another by name, you cannot specify the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + +#### ERB'in It Up + +ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: + +```erb +<% 1000.times do |n| %> +user_<%= n %>: + username: <%= "user#{n}" %> + email: <%= "user#{n}@example.com" %> +<% end %> +``` + +#### Fixtures in Action + +Rails by default automatically loads all fixtures from the `test/fixtures` directory for your models and controllers test. Loading involves three steps: + +1. Remove any existing data from the table corresponding to the fixture +2. Load the fixture data into the table +3. Dump the fixture data into a method in case you want to access it directly + +TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)) + +#### Fixtures are Active Record objects + +Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example: + +```ruby +# this will return the User object for the fixture named david +users(:david) + +# this will return the property for david called id +users(:david).id + +# one can also access methods available on the User class +email(david.partner.email, david.location_tonight) +``` + +To get multiple fixtures at once, you can pass in a list of fixture names. For example: + +```ruby +# this will return an array containing the fixtures david and steve +users(:david, :steve) +``` + + +Model Testing +------------- + +Model tests are used to test the various models of your application. + +For creating Rails model tests, we use the 'test/model' directory for your application. Rails provides a generator to create an model test skeleton for you. + +```bash +$ bin/rails generate test_unit:model article title:string body:text +create test/models/article_test.rb +create test/fixtures/articles.yml +``` + +Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from `ActiveSupport::TestCase`. + + +Integration Testing +------------------- + +Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application. + +For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you. + +```bash +$ bin/rails generate integration_test user_flows + exists test/integration/ + create test/integration/user_flows_test.rb +``` + +Here's what a freshly-generated integration test looks like: + +```ruby +require 'test_helper' + +class UserFlowsTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end +``` + +Inheriting from `ActionDispatch::IntegrationTest` comes with some advantages. This makes available some additional helpers to use in your integration tests. + +### Helpers Available for Integration Tests + +In addition to the standard testing helpers, inheriting `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's briefly introduce you to the three categories of helpers you get to choose from. + +For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html). + +When performing requests, you will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for your use. + +If you'd like to modify the session, or state of your integration test you should look for [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help. + +### Implementing an integration test + +Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly. + +We'll start by generating our integration test skeleton: + +```bash +$ bin/rails generate integration_test blog_flow +``` + +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/integration/blog_flow_test.rb +``` + +Now let's open that file and write our first assertion: + +```ruby +require 'test_helper' + +class BlogFlowTest < ActionDispatch::IntegrationTest + test "can see the welcome page" do + get "/" + assert_select "h1", "Welcome#index" + end +end +``` + +If you remember from earlier in the "Testing Views" section we covered `assert_select` to query the resulting HTML of a request. + +When visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass. + +#### Creating articles integration + +How about testing our ability to create a new article in our blog and see the resulting article. + +```ruby +test "can create an article" do + get "/articles/new" + assert_response :success + + post "/articles", + params: { article: { title: "can create", body: "article successfully." } } + assert_response :redirect + follow_redirect! + assert_response :success + assert_select "p", "Title:\n can create" +end +``` + +Let's break this test down so we can understand it. + +We start by calling the `:new` action on our Articles controller. This response should be successful. + +After this we make a post request to the `:create` action of our Articles controller: + +```ruby +post "/articles", + params: { article: { title: "can create", body: "article successfully." } } +assert_response :redirect +follow_redirect! +``` + +The two lines following the request are to handle the redirect we setup when creating a new article. + +NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made. + +Finally we can assert that our response was successful and our new article is readable on the page. + +#### Taking it further + +We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editing comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. + + Functional Tests for Your Controllers ------------------------------------- @@ -503,19 +646,29 @@ You should test for things such as: Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. +The following command will generate a controller test case with a filled up +test for each of the seven default actions. + +```bash +$ bin/rails generate test_unit:scaffold article +create test/controllers/articles_controller_test.rb +``` + Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby +# articles_controller_test.rb class ArticlesControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success - assert_not_nil assigns(:articles) + assert_includes @response.body, 'Articles' end end ``` -In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable. +In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful +and also ensuring that the right response body has been generated. The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments: @@ -553,7 +706,7 @@ test "should create article" do post :create, params: { article: { title: 'Some title' } } end - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(Article.last) end ``` @@ -580,32 +733,28 @@ To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`, `patch`, `put`, and `delete` methods: ```ruby -test "ajax request responds with no layout" do +test "ajax request" do get :show, params: { id: articles(:first).id }, xhr: true - assert_template :index - assert_template layout: nil + assert_equal 'hello world', @response.body + assert_equal "text/javascript", @response.content_type end ``` -### The Four Hashes of the Apocalypse +### The Three Hashes of the Apocalypse -After a request has been made and processed, you will have 4 Hash objects ready for use: +After a request has been made and processed, you will have 3 Hash objects ready for use: -* `assigns` - Any objects that are stored as instance variables in actions for use in views. * `cookies` - Any cookies that are set. * `flash` - Any objects living in the flash. * `session` - Any object living in session variables. -As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name, except for `assigns`. For example: +As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name. For example: ```ruby flash["gordon"] flash[:gordon] session["shmession"] session[:shmession] cookies["are_good_for_u"] cookies[:are_good_for_u] - -# Because you can't use assigns[:something] for historical reasons: -assigns["something"] assigns(:something) ``` ### Instance Variables Available @@ -633,49 +782,9 @@ get :index # simulate the request with custom header post :create # simulate the request with custom env variable ``` -### Testing Templates and Layouts - -Eventually, you may want to test whether a specific layout is rendered in the view of a response. - -#### Asserting Templates - -If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template` -method: - -```ruby -test "index should render correct template and layout" do - get :index - assert_template :index - assert_template layout: "layouts/application" - - # You can also pass a regular expression. - assert_template layout: /layouts\/application/ -end -``` - -NOTE: You cannot test for template and layout at the same time, with a single call to `assert_template`. - -WARNING: You must include the "layouts" directory name even if you save your layout file in this standard layout directory. Hence, `assert_template layout: "application"` will not work. - -#### Asserting Partials - -If your view renders any partial, when asserting for the layout, you can to assert for the partial at the same time. -Otherwise, assertion will fail. - -Remember, we added the "_form" partial to our new Article view? Let's write an assertion for that in the `:new` action now: - -```ruby -test "new should render correct layout" do - get :new - assert_template layout: "layouts/application", partial: "_form" -end -``` - -This is the correct way to assert for when the view renders a partial with a given name. As identified by the `:partial` key passed to the `assert_template` call. - ### Testing `flash` notices -If you remember from earlier one of the Four Hashes of the Apocalypse was `flash`. +If you remember from earlier one of the Three Hashes of the Apocalypse was `flash`. We want to add a `flash` message to our blog application whenever someone successfully creates a new Article. @@ -688,7 +797,7 @@ test "should create article" do post :create, params: { article: { title: 'Some title' } } end - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(Article.last) assert_equal 'Article was successfully created.', flash[:notice] end ``` @@ -781,7 +890,7 @@ We can also add a test for updating an existing Article. test "should update article" do article = articles(:one) patch :update, params: { id: article.id, article: { title: "updated" } } - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(article) end ``` @@ -820,7 +929,7 @@ class ArticlesControllerTest < ActionController::TestCase test "should update article" do patch :update, params: { id: @article.id, article: { title: "updated" } } - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(@article) end end ``` @@ -857,7 +966,6 @@ class ProfileControllerTest < ActionController::TestCase get :show assert_response :success - assert_equal users(:david), assigns(:user) end end ``` @@ -865,33 +973,7 @@ end Testing Routes -------------- -Like everything else in your Rails application, it is recommended that you test your routes. Below are example tests for the routes of default `show` and `create` action of `Articles` controller above and it should look like: - -```ruby -class ArticleRoutesTest < ActionController::TestCase - test "should route to article" do - assert_routing '/articles/1', { controller: "articles", action: "show", id: "1" } - end - - test "should route to create article" do - assert_routing({ method: 'post', path: '/articles' }, { controller: "articles", action: "create" }) - end -end -``` - -I've added this file here `test/controllers/articles_routes_test.rb` and if we run the test we should see: - -```bash -$ bin/rails test test/controllers/articles_routes_test.rb - -# Running: - -.. - -Finished in 0.069381s, 28.8263 runs/s, 86.4790 assertions/s. - -2 runs, 6 assertions, 0 failures, 0 errors, 0 skips -``` +Like everything else in your Rails application, you can test your routes. For more information on routing assertions available in Rails, see the API documentation for [`ActionDispatch::Assertions::RoutingAssertions`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html). @@ -981,8 +1063,6 @@ have to use a mixin like this: ```ruby class UserHelperTest < ActionView::TestCase - include UserHelper - test "should return the user name" do # ... end @@ -992,120 +1072,6 @@ end Moreover, since the test class extends from `ActionView::TestCase`, you have access to Rails' helper methods such as `link_to` or `pluralize`. -Integration Testing -------------------- - -Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application. - -For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you. - -```bash -$ bin/rails generate integration_test user_flows - exists test/integration/ - create test/integration/user_flows_test.rb -``` - -Here's what a freshly-generated integration test looks like: - -```ruby -require 'test_helper' - -class UserFlowsTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end -end -``` - -Inheriting from `ActionDispatch::IntegrationTest` comes with some advantages. This makes available some additional helpers to use in your integration tests. - -### Helpers Available for Integration Tests - -In addition to the standard testing helpers, inheriting `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's briefly introduce you to the three categories of helpers you get to choose from. - -For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html). - -When performing requests, you will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for your use. - -If you'd like to modify the session, or state of your integration test you should look for [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help. - -### Implementing an integration test - -Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly. - -We'll start by generating our integration test skeleton: - -```bash -$ bin/rails generate integration_test blog_flow -``` - -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/integration/blog_flow_test.rb -``` - -Now let's open that file and write our first assertion: - -```ruby -require 'test_helper' - -class BlogFlowTest < ActionDispatch::IntegrationTest - test "can see the welcome page" do - get "/" - assert_select "h1", "Welcome#index" - end -end -``` - -If you remember from earlier in the "Testing Views" section we covered `assert_select` to query the resulting HTML of a request. - -When visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass. - -#### Creating articles integration - -How about testing our ability to create a new article in our blog and see the resulting article. - -```ruby -test "can create an article" do - get "/articles/new" - assert_response :success - assert_template "articles/new", partial: "articles/_form" - - post "/articles", - params: { article: { title: "can create", body: "article successfully." } } - assert_response :redirect - follow_redirect! - assert_response :success - assert_template "articles/show" - assert_select "p", "Title:\n can create" -end -``` - -Let's break this test down so we can understand it. - -We start by calling the `:new` action on our Articles controller. This response should be successful, and we can verify the correct template is rendered including the form partial. - -After this we make a post request to the `:create` action of our Articles controller: - -```ruby -post "/articles", - params: { article: { title: "can create", body: "article successfully." } } -assert_response :redirect -follow_redirect! -``` - -The two lines following the request are to handle the redirect we setup when creating a new article. - -NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made. - -Finally we can assert that our response was successful, template was rendered, and our new article is readable on the page. - -#### Taking it further - -We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editting comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. - Testing Your Mailers -------------------- @@ -1258,16 +1224,3 @@ class ProductTest < ActiveJob::TestCase end end ``` - -Other Testing Approaches ------------------------- - -The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including: - -* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use. -* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures. -* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run. -* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests. -* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions. -* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework -* [Capybara](http://jnicklas.github.com/capybara/), Acceptance test framework for web applications diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 17309d4b47..743241d7a0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -75,7 +75,7 @@ warning by adding the following configuration to your `config/application.rb`: See [#17227](https://github.com/rails/rails/pull/17227) for more details. -### ActiveJob jobs now inherent from ApplicationJob by default +### ActiveJob jobs now inherit from ApplicationJob by default In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this behavior has changed to now inherit from `ApplicationJob`. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index e3856a285a..1c42ff2914 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -191,34 +191,6 @@ $(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). -Another possibility is returning javascript directly from the server side on -remote calls: - -```ruby -# articles_controller -def create - respond_to do |format| - if @article.save - format.html { ... } - format.js do - render js: <<-endjs - alert('Article saved successfully!'); - window.location = '#{article_path(@article)}'; - endjs - end - else - format.html { ... } - format.js do - render js: "alert('There are empty fields in the form!');" - end - end - end -end -``` - -NOTE: If javascript is disabled in the user browser, `format.html { ... }` -block should be executed as fallback. - ### form_tag [`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag) @@ -286,7 +258,7 @@ this generates ```html <form action="/articles/1" class="button_to" data-remote="true" method="post"> - <div><input type="submit" value="An article"></div> + <input type="submit" value="An article" /> </form> ``` @@ -385,7 +357,7 @@ This gem uses Ajax to speed up page rendering in most applications. Turbolinks attaches a click handler to all `<a>` 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()_method), +[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 replace the entire `<body>` of the page with the `<body>` of the response. It will then use PushState to change the URL to the correct one, preserving diff --git a/install.rb b/install.rb deleted file mode 100644 index 96e4153165..0000000000 --- a/install.rb +++ /dev/null @@ -1,16 +0,0 @@ -version = ARGV.pop - -if version.nil? - puts "Usage: ruby install.rb version" - exit(64) -end - -%w( activesupport activemodel activerecord actionpack actionview actionmailer railties activejob ).each do |framework| - puts "Installing #{framework}..." - `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem` -end - -puts "Installing rails..." -`gem build rails.gemspec` -`gem install rails-#{version}.gem --no-ri --no-rdoc ` -`rm rails-#{version}.gem` diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 02fdaf09e0..21adac272d 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,82 @@ +* Plugins generated using `rails plugin new` are now generated with the + version number set to 0.1.0. + + *Daniel Morris* + +* `I18n.load_path` is now reloaded under development so there's no need to + restart the server to make new locale files available. Also, I18n will no + longer raise for deleted locale files. + + *Kir Shatrov* + +* Add `bin/update` script to update development environment automatically. + + *Mehmet Emin İNAÇ* + +* Fix STATS_DIRECTORIES already defined warning when running rake from within + the top level directory of an engine that has a test app. + + Fixes #20510 + + *Ersin Akinci* + +* Make enabling or disabling caching in development mode possible with + rake dev:cache. + + Running rake dev:cache will create or remove tmp/caching-dev.txt. When this + file exists config.action_controller.perform_caching will be set to true in + config/environments/development.rb. + + Additionally, a server can be started with either --dev-caching or + --no-dev-caching included to toggle caching on startup. + + *Jussi Mertanen*, *Chuck Callebs* + +* Add a `--api` option in order to generate plugins that can be added + inside an API application. + + *Robin Dupret* + +* Fix `NoMethodError` when generating a scaffold inside a full engine. + + *Yuji Yaginuma* + +* Adding support for passing a block to the `add_source` action of a custom generator + + *Mike Dalton*, *Hirofumi Wakasugi* + +* `assert_file` understands paths with special characters + (eg. `v0.1.4~alpha+nightly`). + + *Diego Carrion* + +* Remove ContentLength middleware from the defaults. If you want it, just + add it as a middleware in your config. + + *Egg McMuffin* + +* Make it possible to customize the executable inside rerun snippets. + + *Yves Senn* + +* Add support for API only apps. + Middleware stack was slimmed down and it has only the needed + middleware for API apps & generators generates the right files, + folders and configurations. + + *Santiago Pastorino & Jorge Bejar* + +* Make generated scaffold functional tests work inside engines. + + *Yuji Yaginuma* + +* Generator a `.keep` file in the `tmp` folder by default as many scripts + assume the existence of this folder and most would fail if it is absent. + + See #20299. + + *Yoong Kang Lim*, *Sunny Juneja* + * `config.static_index` configures directory `index.html` filename Set `config.static_index` to serve a static directory index file not named diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 0f4d932749..9baf8aa742 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -63,7 +63,7 @@ INFO Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store) if Rails.cache.respond_to?(:middleware) - config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware) + config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware) end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 78a47fcda9..4fc7a1db62 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -16,7 +16,7 @@ module Rails :beginning_of_week, :filter_redirect, :x attr_writer :log_level - attr_reader :encoding + attr_reader :encoding, :api_only def initialize(*) super @@ -49,6 +49,7 @@ module Rails @eager_load = nil @secret_token = nil @secret_key_base = nil + @api_only = false @x = Custom.new end @@ -60,6 +61,11 @@ module Rails end end + def api_only=(value) + @api_only = value + generators.api_only = value + end + def paths @paths ||= begin paths = super diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 503977b395..88eade5c5a 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -18,7 +18,7 @@ module Rails middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header if config.serve_static_files - middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control, config.static_index + middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control, index: config.static_index end if rack_cache = load_rack_cache @@ -26,9 +26,29 @@ module Rails middleware.use ::Rack::Cache, rack_cache end - middleware.use ::Rack::Lock unless allow_concurrency? + if config.allow_concurrency == false + # User has explicitly opted out of concurrent request + # handling: presumably their code is not threadsafe + + middleware.use ::Rack::Lock + + elsif config.allow_concurrency == :unsafe + # Do nothing, even if we know this is dangerous. This is the + # historical behaviour for true. + + else + # Default concurrency setting: enabled, but safe + + unless config.cache_classes && config.eager_load + # Without cache_classes + eager_load, the load interlock + # is required for proper operation + + middleware.use ::ActionDispatch::LoadInterlock + end + end + middleware.use ::Rack::Runtime - middleware.use ::Rack::MethodOverride + middleware.use ::Rack::MethodOverride unless config.api_only middleware.use ::ActionDispatch::RequestId # Must come after Rack::MethodOverride to properly log overridden methods @@ -42,9 +62,9 @@ module Rails end middleware.use ::ActionDispatch::Callbacks - middleware.use ::ActionDispatch::Cookies + middleware.use ::ActionDispatch::Cookies unless config.api_only - if config.session_store + if !config.api_only && config.session_store if config.force_ssl && !config.session_options.key?(:secure) config.session_options[:secure] = true end @@ -65,14 +85,6 @@ module Rails config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any? end - def allow_concurrency? - if config.allow_concurrency.nil? - config.cache_classes && config.eager_load - else - config.allow_concurrency - end - end - def load_rack_cache rack_cache = config.action_dispatch.rack_cache return unless rack_cache diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 0599e988d9..404e3c3e23 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -86,8 +86,10 @@ module Rails # added in the hook are taken into account. initializer :set_clear_dependencies_hook, group: :all do callback = lambda do - ActiveSupport::DescendantsTracker.clear - ActiveSupport::Dependencies.clear + ActiveSupport::Dependencies.interlock.attempt_unloading do + ActiveSupport::DescendantsTracker.clear + ActiveSupport::Dependencies.clear + end end if config.reload_classes_only_on_change diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index 9a29ec21cf..618a09a5b3 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -6,7 +6,7 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: def require_local! unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden + render html: '<p>For security purposes, this information is only available to local requests.</p>'.html_safe, status: :forbidden end end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 546d3725d8..c8fb58ab05 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -34,6 +34,9 @@ module Rails 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 "" @@ -67,6 +70,7 @@ module Rails print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -77,18 +81,7 @@ module Rails end def middleware - middlewares = [] - middlewares << [::Rack::ContentLength] - - # FIXME: add Rack::Lock in the case people are using webrick. - # This is to remain backwards compatible for those who are - # running webrick in production. We should consider removing this - # in development. - if server.name == 'Rack::Handler::WEBrick' - middlewares << [::Rack::Lock] - end - - Hash.new(middlewares) + Hash.new([]) end def default_options @@ -97,13 +90,23 @@ module Rails DoNotReverseLookup: true, environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, daemonize: false, - pid: File.expand_path("tmp/pids/server.pid"), - config: File.expand_path("config.ru") + caching: false, + pid: File.expand_path("tmp/pids/server.pid") }) end private + def setup_dev_caching + return unless options[:environment] == "development" + + if options[:caching] == false + delete_cache_file + elsif options[:caching] + create_cache_file + end + end + def print_boot_information url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" @@ -113,6 +116,14 @@ module Rails puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end + def create_cache_file + FileUtils.touch("tmp/caching-dev.txt") + end + + def delete_cache_file + FileUtils.rm("tmp/caching-dev.txt") if File.exist?("tmp/caching-dev.txt") + end + def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb index 598e224a6f..dd069f081f 100644 --- a/railties/lib/rails/commands/test.rb +++ b/railties/lib/rails/commands/test.rb @@ -1,5 +1,9 @@ -require "rails/test_unit/runner" +require "rails/test_unit/minitest_plugin" -$: << File.expand_path("../../test", APP_PATH) +if defined?(ENGINE_ROOT) + $: << File.expand_path('test', ENGINE_ROOT) +else + $: << File.expand_path('../../test', APP_PATH) +end -Rails::TestRunner.run(ARGV) +exit Minitest.run(ARGV) diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 76364cea8f..d99d27a756 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -74,7 +74,7 @@ module Rails end class Generators #:nodoc: - attr_accessor :aliases, :options, :templates, :fallbacks, :colorize_logging + attr_accessor :aliases, :options, :templates, :fallbacks, :colorize_logging, :api_only attr_reader :hidden_namespaces def initialize @@ -83,6 +83,7 @@ module Rails @fallbacks = {} @templates = [] @colorize_logging = true + @api_only = false @hidden_namespaces = [] end diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb index 2a69c26deb..ac5836a588 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -18,6 +18,11 @@ module Rails app = Rails.application session = ActionDispatch::Integration::Session.new(app) yield session if block_given? + + # This makes app.url_for and app.foo_path available in the console + session.extend(app.routes.url_helpers) + session.extend(app.routes.mounted_helpers) + session end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 1dede32dd4..e90d41cbec 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -519,17 +519,15 @@ module Rails # Define the Rack API for this engine. def call(env) env.merge!(env_config) - if env['SCRIPT_NAME'] - env[routes.env_key] = env['SCRIPT_NAME'].dup - end + req = ActionDispatch::Request.new env + req.routes = routes + req.engine_script_name = req.script_name app.call(env) end # Defines additional Rack env configuration that is added on each call. def env_config - @env_config ||= { - 'action_dispatch.routes' => routes - } + @env_config ||= {} end # Defines the routes for this engine. If a block is given to @@ -589,7 +587,7 @@ module Rails # I18n load paths are a special case since the ones added # later have higher priority. initializer :add_locales do - config.i18n.railties_load_path.concat(paths["config/locales"].existent) + config.i18n.railties_load_path << paths["config/locales"] end initializer :add_view_paths do diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index f39f926109..a6d87b78e4 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -2,7 +2,8 @@ ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", - "d" => "destroy" + "d" => "destroy", + "t" => "test" } command = ARGV.shift @@ -12,7 +13,7 @@ require ENGINE_PATH engine = ::Rails::Engine.find(ENGINE_ROOT) case command -when 'generate', 'destroy' +when 'generate', 'destroy', 'test' require 'rails/generators' Rails::Generators.namespace = engine.railtie_namespace engine.load_generators @@ -30,6 +31,7 @@ Usage: rails COMMAND [ARGS] 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. diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index a7da92168d..b430cf1909 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -33,6 +33,7 @@ module Rails scaffold_controller: '-c', stylesheets: '-y', stylesheet_engine: '-se', + scaffold_stylesheet: '-ss', template_engine: '-e', test_framework: '-t' }, @@ -44,6 +45,7 @@ module Rails DEFAULT_OPTIONS = { rails: { + api: false, assets: true, force_plural: false, helper: true, @@ -56,12 +58,14 @@ module Rails scaffold_controller: :scaffold_controller, stylesheets: true, stylesheet_engine: :css, + scaffold_stylesheet: true, test_framework: false, 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 @@ -99,6 +103,21 @@ module Rails @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 views, 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 + ) + end + # Remove the color from output. def self.no_color! Thor::Base.shell = Thor::Shell::Basic diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 70a20801a0..b4356f71e0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -63,12 +63,26 @@ module Rails # Add the given source to +Gemfile+ # + # If block is given, gem entries in block are wrapped into the source group. + # # add_source "http://gems.github.com/" - def add_source(source, options={}) + # + # add_source "http://gems.github.com/" do + # gem "rspec-rails" + # end + def add_source(source, options={}, &block) log :source, source in_root do - prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false + if block + append_file "Gemfile", "source #{quote(source)} do", force: true + @in_group = true + instance_eval(&block) + @in_group = false + append_file "Gemfile", "\nend\n", force: true + else + prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false + end end end @@ -78,11 +92,11 @@ module Rails # file in <tt>config/environments</tt>. # # environment do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'cdn.provider.com'" # end # # environment(nil, env: "development") do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'localhost:3000'" # end def environment(data=nil, options={}) sentinel = /class [a-z_:]+ < Rails::Application/i diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 061fafb156..249fe96772 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -208,11 +208,13 @@ module Rails if options.dev? [ GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), + GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), GemfileEntry.github('arel', 'rails/arel') ] elsif options.edge? [ GemfileEntry.github('rails', 'rails/rails'), + GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), GemfileEntry.github('arel', 'rails/arel') ] else @@ -264,6 +266,8 @@ module Rails end def jbuilder_gemfile_entry + return [] if options[:api] + comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' GemfileEntry.version('jbuilder', '~> 2.0', comment) end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 813b8b629e..6fa413f8b0 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -103,12 +103,12 @@ module Rails # hook_for :test_framework, as: :controller # end # - # And now it will lookup at: + # And now it will look up at: # # "test_unit:controller", "test_unit" # - # Similarly, if you want it to also lookup in the rails namespace, you just - # need to provide the :in value: + # Similarly, if you want it to also look up in the rails namespace, you + # just need to provide the :in value: # # class AwesomeGenerator < Rails::Generators::Base # hook_for :test_framework, in: :rails, as: :controller diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index c3b8ef1181..5f4904fee1 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -28,4 +28,4 @@ <br> -<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %> +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 01a8e2e9b4..243694f38e 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -18,8 +18,8 @@ module Rails parse_attributes! if respond_to?(:attributes) end - # Defines the template that would be used for the migration file. - # The arguments include the source template file, the migration filename etc. + # Overrides <tt>Thor::Actions#template</tt> so it can tell if + # a template is currently being created. no_tasks do def template(source, *args, &block) inside_template do @@ -183,6 +183,10 @@ module Rails !defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names end + def mountable_engine? + defined?(ENGINE_ROOT) && namespaced? + end + # Add a class collisions name to be checked on class initialization. You # can supply a hash with a :prefix or :suffix to be tested. # diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 1047a2c429..b813b083f4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -141,6 +141,7 @@ module Rails end def tmp + empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" empty_directory "tmp/cache/assets" end @@ -174,6 +175,9 @@ module Rails class_option :version, type: :boolean, aliases: "-v", group: :rails, desc: "Show Rails version number and quit" + class_option :api, type: :boolean, + desc: "Preconfigure smaller stack for API only apps" + def initialize(*args) super @@ -184,6 +188,10 @@ module Rails 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. + # 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] end public_task :set_default_accessors! @@ -251,6 +259,28 @@ module Rails build(:vendor) end + def delete_app_assets_if_api_option + if options[:api] + remove_dir 'app/assets' + remove_dir 'lib/assets' + remove_dir 'tmp/cache/assets' + remove_dir 'vendor/assets' + end + end + + def delete_app_helpers_if_api_option + if options[:api] + remove_dir 'app/helpers' + remove_dir 'test/helpers' + end + end + + def delete_app_views_if_api_option + if options[:api] + remove_dir 'app/views' + end + end + def delete_js_folder_skipping_javascript if options[:skip_javascript] remove_dir 'app/assets/javascripts' @@ -269,6 +299,13 @@ module Rails end end + def delete_non_api_initializers_if_api_option + if options[:api] + remove_file 'config/initializers/session_store.rb' + remove_file 'config/initializers/cookies_serializer.rb' + end + end + def finish_template build(:leftovers) end @@ -319,7 +356,9 @@ module Rails if app_const =~ /^\d/ raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" + raise Error, "Invalid application name #{app_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(app_const_base) raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 29203b9c37..6a1c2faaab 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -21,6 +21,14 @@ source 'https://rubygems.org' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development +<%- if options.api? -%> +# Use ActiveModelSerializers to serialize JSON responses +gem 'active_model_serializers', '~> 0.10.0.rc2' + +# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible +# gem 'rack-cors' + +<%- end -%> <% if RUBY_ENGINE == 'ruby' -%> group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -28,13 +36,15 @@ group :development, :test do end group :development do +<%- unless options.api? -%> # Access an IRB console on exception pages or by using <%%= console %> in views <%- if options.dev? || options.edge? -%> - gem 'web-console', github: "rails/web-console" + gem 'web-console', github: 'rails/web-console' <%- else -%> gem 'web-console', '~> 2.0' <%- end -%> -<%- if spring_install? %> +<%- end -%> +<% if spring_install? -%> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' <% end -%> 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 cb86978d4c..c88426ec06 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 @@ -5,7 +5,7 @@ // 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. +// compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. 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 0cdd2788d0..0ebd7fe829 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 @@ -7,7 +7,8 @@ * * 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 - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt index d83690e1b9..f726fd6305 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt @@ -1,5 +1,7 @@ -class ApplicationController < ActionController::Base +class ApplicationController < ActionController::<%= options[:api] ? "API" : "Base" %> +<%- unless options[:api] -%> # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception +<%- end -%> end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup index 3a5a2cc1e3..0c8b179827 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -5,13 +5,17 @@ include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' - system 'gem install bundler --conservative' - system('bundle check') or system('bundle install') + system! 'gem install bundler --conservative' + system('bundle check') or system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') @@ -19,11 +23,11 @@ chdir APP_ROOT do # end puts "\n== Preparing database ==" - system 'ruby bin/rake db:setup' + system! 'ruby bin/rake db:setup' puts "\n== Removing old logs and tempfiles ==" - system 'ruby bin/rake log:clear tmp:clear' + system! 'ruby bin/rake log:clear tmp:clear' puts "\n== Restarting application server ==" - touch 'tmp/restart.txt' + system! 'ruby bin/rake restart' end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update new file mode 100644 index 0000000000..9830e6b29a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/update @@ -0,0 +1,28 @@ +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system 'bundle check' or system! 'bundle install' + + puts "\n== Updating database ==" + system! 'bin/rake db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rake log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rake restart' +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 a2661bfb51..6b7d7abd0b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -32,5 +32,12 @@ module <%= app_const_base %> # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de +<%- if options[:api] -%> + + # Only loads a smaller set of middleware suitable for API only apps. + # Middleware like session, flash, cookies can be added back manually. + # Skip views, helpers and assets when generating a new resource. + config.api_only = true +<%- end -%> end end 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 ecb5d4170f..e29f0bacaa 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 @@ -9,9 +9,19 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. + # Show full error reports. config.consider_all_requests_local = true - config.action_controller.perform_caching = false + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.static_cache_control = "public, max-age=172800" + config.cache_store = :memory_store + else + config.action_controller.perform_caching = false + config.cache_store = :null_store + end + <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. 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 8c09396fc1..0297ab75f6 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 @@ -51,7 +51,8 @@ Rails.application.configure do # config.log_tags = [ :subdomain, :request_id ] # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') # Use a different cache store in production. # config.cache_store = :mem_cache_store diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb new file mode 100644 index 0000000000..45c44d24f8 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb @@ -0,0 +1,14 @@ +# Avoid CORS issues when API is called from the frontend app +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, "Rack::Cors" do +# allow do +# origins 'example.com' +# +# resource '*', +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt index 94f612c3dd..cadc85cfac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt @@ -5,7 +5,7 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end <%- unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb index 3f66539d54..787824f888 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -1,56 +1,3 @@ Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index e4a00ad181..1b8cf8a9fa 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -15,7 +15,8 @@ <% end -%> # Ignore all logfiles and tempfiles. /log/* +/tmp/* <% if keeps? -%> !/log/.keep +!/tmp/.keep <% end -%> -/tmp diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 7f5bf0c8b8..0e1326ce4f 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -17,15 +17,22 @@ module Rails def app if mountable? - directory 'app' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + if api? + directory 'app', exclude_pattern: %r{app/(views|helpers)} + else + directory 'app' + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + end elsif full? empty_directory_with_keep_file 'app/models' empty_directory_with_keep_file 'app/controllers' - empty_directory_with_keep_file 'app/views' - empty_directory_with_keep_file 'app/helpers' empty_directory_with_keep_file 'app/mailers' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + + unless api? + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + empty_directory_with_keep_file 'app/helpers' + empty_directory_with_keep_file 'app/views' + end end end @@ -82,6 +89,7 @@ task default: :test opts = (options || {}).slice(*PASSTHROUGH_OPTIONS) opts[:force] = force opts[:skip_bundle] = true + opts[:api] = options.api? invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -176,6 +184,9 @@ task default: :test desc: "If creating plugin in application's directory " + "skip adding entry to Gemfile" + class_option :api, type: :boolean, default: false, + desc: "Generate a smaller stack for API application plugins" + def initialize(*args) @dummy_path = nil super @@ -210,15 +221,15 @@ task default: :test end def create_public_stylesheets_files - build(:stylesheets) + build(:stylesheets) unless api? end def create_javascript_files - build(:javascripts) + build(:javascripts) unless api? end def create_images_directory - build(:images) + build(:images) unless api? end def create_bin_files @@ -305,6 +316,10 @@ task default: :test options[:skip_test].blank? || options[:dummy_path] != 'test/dummy' end + def api? + options[:api] + end + def self.banner "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]" end @@ -364,7 +379,9 @@ task default: :test elsif camelized =~ /^\d/ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(name) - raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" + raise Error, "Invalid plugin name #{original_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(camelized) raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name." end diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt index 7157e48c42..7fe4e5034d 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt @@ -1,5 +1,5 @@ <%= wrap_in_modules <<-rb.strip_heredoc - class ApplicationController < ActionController::Base + class ApplicationController < ActionController::#{api? ? "API" : "Base"} end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb index 17afd52177..8938770fc4 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb @@ -1,6 +1,7 @@ <%= wrap_in_modules <<-rb.strip_heredoc class Engine < ::Rails::Engine #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '} + #{api? ? " config.generators.api_only = true" : ' '} end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb index d257295988..b08f4ef9ae 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb @@ -1 +1 @@ -<%= wrap_in_modules 'VERSION = "0.0.1"' %> +<%= wrap_in_modules "VERSION = '0.1.0'" %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js index 8913b40f69..e54c6461cc 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js @@ -5,7 +5,7 @@ // 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. +// compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css index 0cdd2788d0..0ebd7fe829 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css @@ -7,7 +7,8 @@ * * 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 - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb index 824caecb24..f5d1ec2046 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb @@ -1,10 +1,6 @@ require 'test_helper' class NavigationTest < ActionDispatch::IntegrationTest -<% unless options[:skip_active_record] -%> - fixtures :all -<% end -%> - # test "the truth" do # assert true # 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 0852ffce9a..b1b77629d4 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 @@ -20,6 +20,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) - ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "files" + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb index c986f95e67..42705107ae 100644 --- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb +++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb @@ -1,7 +1,6 @@ module Rails module Generators class ResourceRouteGenerator < NamedBase # :nodoc: - # Properly nests namespaces passed into a generator # # $ rails generate resource admin/users/products diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index e89789e72b..17c32bfdb3 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -10,10 +10,11 @@ module Rails class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean class_option :resource_route, type: :boolean + class_option :scaffold_stylesheet, type: :boolean def handle_skip @options = @options.merge(stylesheets: false) unless options[:assets] - @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] + @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet] end hook_for :scaffold_controller, required: true 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 c01b82884d..d0b8cad896 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 @@ -10,11 +10,14 @@ module Rails class_option :helper, type: :boolean class_option :orm, banner: "NAME", type: :string, required: true, desc: "ORM to generate the controller for" + class_option :api, type: :boolean, + desc: "Generates API controller" argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_controller_files - template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") + template_file = options.api? ? "api_controller.rb" : "controller.rb" + template template_file, File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") end hook_for :template_engine, :test_framework, as: :scaffold diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb new file mode 100644 index 0000000000..bc3c9b3f6b --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb @@ -0,0 +1,61 @@ +<% if namespaced? -%> +require_dependency "<%= namespaced_file_path %>/application_controller" + +<% end -%> +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + + render json: <%= "@#{plural_table_name}" %> + end + + # GET <%= route_url %>/1 + def show + render json: <%= "@#{singular_table_name}" %> + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + render json: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + end + + # Only allow a trusted parameter "white list" through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params[:<%= singular_table_name %>] + <%- else -%> + params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + <%- end -%> + end +end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb index 509bd60564..5a8a3ca5e0 100644 --- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb @@ -2,6 +2,12 @@ require 'test_helper' <% module_namespacing do -%> class <%= class_name %>ControllerTest < ActionController::TestCase +<% if mountable_engine? -%> + setup do + @routes = Engine.routes + end + +<% end -%> <% if actions.empty? -%> # test "the truth" do # assert true 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 2e1f55f2a6..0171da7cc7 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -8,13 +8,26 @@ module TestUnit # :nodoc: check_class_collision suffix: "ControllerTest" + class_option :api, type: :boolean, + desc: "Generates API functional tests" + argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_test_files - template "functional_test.rb", + template_file = options.api? ? "api_functional_test.rb" : "functional_test.rb" + template template_file, File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb") end + def fixture_name + @fixture_name ||= + if mountable_engine? + "%s_%s" % [namespaced_path, table_name] + else + table_name + end + end + private def attributes_hash diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb new file mode 100644 index 0000000000..f302cd6c3d --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +<% module_namespacing do -%> +class <%= controller_class_name %>ControllerTest < ActionController::TestCase + setup do + @<%= singular_table_name %> = <%= fixture_name %>(:one) +<% if mountable_engine? -%> + @routes = Engine.routes +<% end -%> + end + + test "should get index" do + get :index + assert_response :success + end + + test "should create <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count') do + post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + end + + assert_response 201 + end + + test "should show <%= singular_table_name %>" do + get :show, params: { id: <%= "@#{singular_table_name}" %> } + assert_response :success + end + + test "should update <%= singular_table_name %>" do + patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + assert_response 200 + end + + test "should destroy <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count', -1) do + delete :destroy, params: { id: <%= "@#{singular_table_name}" %> } + end + + assert_response 204 + end +end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index 76313575e9..50b98b2631 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -3,7 +3,10 @@ require 'test_helper' <% module_namespacing do -%> class <%= controller_class_name %>ControllerTest < ActionController::TestCase setup do - @<%= singular_table_name %> = <%= table_name %>(:one) + @<%= singular_table_name %> = <%= fixture_name %>(:one) +<% if mountable_engine? -%> + @routes = Engine.routes +<% end -%> end test "should get index" do diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index bd069e4bd0..17af6eddfa 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -23,7 +23,7 @@ module Rails # end # end def assert_file(relative, *contents) - absolute = File.expand_path(relative, destination_root).shellescape + absolute = File.expand_path(relative, destination_root) assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not" read = File.read(absolute) if block_given? || !contents.empty? diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 41422a656c..78a857e0f1 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -26,7 +26,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: if part = find_part(part_type) response.content_type = part_type - render text: part.respond_to?(:decoded) ? part.decoded : part + render plain: part.respond_to?(:decoded) ? part.decoded : part else raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index ebcaaaba46..e47616a87f 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -123,6 +123,10 @@ module Rails options[:load_path] ? load_path! : skip_load_path! end + def absolute_current # :nodoc: + File.expand_path(@current, @root.path) + end + def children keys = @root.keys.find_all { |k| k.start_with?(@current) && k != @current @@ -175,6 +179,10 @@ module Rails @paths end + def extensions # :nodoc: + $1.split(',') if @glob =~ /\{([\S]+)\}/ + end + # Expands all paths against the root and return all unique values. def expanded raise "You need to set a path root" unless @root.path diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 2c3d278eca..d3e33584d7 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,6 +3,7 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations + dev framework initializers log @@ -10,8 +11,9 @@ require 'rake' misc restart routes - statistics tmp -).each do |task| +).tap { |arr| + arr << 'statistics' if Rake.application.current_scope.empty? +}.each do |task| load "rails/tasks/#{task}.rake" end diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake new file mode 100644 index 0000000000..e949172d3f --- /dev/null +++ b/railties/lib/rails/tasks/dev.rake @@ -0,0 +1,15 @@ +namespace :dev do + task :cache do + desc 'Toggle development mode caching on/off' + + if File.exist? 'tmp/caching-dev.txt' + File.delete 'tmp/caching-dev.txt' + puts 'Development mode is no longer being cached.' + else + FileUtils.touch 'tmp/caching-dev.txt' + puts 'Development mode is now being cached.' + end + + FileUtils.touch 'tmp/restart.txt' + end +end diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake index 1e8940b675..f36c86d81b 100644 --- a/railties/lib/rails/tasks/restart.rake +++ b/railties/lib/rails/tasks/restart.rake @@ -1,4 +1,5 @@ desc "Restart app by touching tmp/restart.txt" task :restart do + FileUtils.mkdir_p('tmp') FileUtils.touch('tmp/restart.txt') end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index a83e39faee..5cc1b5b219 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -3,13 +3,14 @@ abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production? require "rails/test_unit/minitest_plugin" -require 'active_support/testing/autorun' require 'active_support/test_case' require 'action_controller' require 'action_controller/test_case' require 'action_dispatch/testing/integration' require 'rails/generators/test_case' +require 'active_support/testing/autorun' + if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! @@ -27,14 +28,14 @@ if defined?(ActiveRecord::Base) end class ActionController::TestCase - def before_setup + def before_setup # :nodoc: @routes = Rails.application.routes super end end class ActionDispatch::IntegrationTest - def before_setup + def before_setup # :nodoc: @routes = Rails.application.routes super end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 70ce9d3360..5552779ef1 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -1,14 +1,52 @@ -require "minitest" +require "active_support/core_ext/module/attribute_accessors" require "rails/test_unit/reporter" +require "rails/test_unit/test_requirer" -def Minitest.plugin_rails_init(options) - self.reporter << Rails::TestUnitReporter.new(options[:io], options) - if $rails_test_runner && (method = $rails_test_runner.find_method) - options[:filter] = method +module Minitest + def self.plugin_rails_options(opts, options) + opts.separator "" + opts.separator "Usage: bin/rails test [options] [files or directories]" + opts.separator "You can run a single test by appending a line number to a filename:" + opts.separator "" + opts.separator " bin/rails test test/models/user_test.rb:27" + opts.separator "" + opts.separator "You can run multiple files and directories at the same time:" + opts.separator "" + opts.separator " bin/rails test test/controllers test/integration/login_test.rb" + opts.separator "" + + opts.separator "Rails options:" + opts.on("-e", "--environment [ENV]", + "Run tests in the ENV environment") do |env| + options[:environment] = env.strip + end + + opts.on("-b", "--backtrace", + "Show the complete backtrace") do + options[:full_backtrace] = true + end + + options[:patterns] = opts.order! end - if !($rails_test_runner && $rails_test_runner.show_backtrace?) - Minitest.backtrace_filter = Rails.backtrace_cleaner + def self.plugin_rails_init(options) + self.run_with_rails_extension = true + + ENV["RAILS_ENV"] = options[:environment] || "test" + + ::Rails::TestRequirer.require_files options[:patterns] unless run_with_autorun + + unless options[:full_backtrace] || ENV["BACKTRACE"] + # Plugin can run without Rails loaded, check before filtering. + Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner) + end + + self.reporter << ::Rails::TestUnitReporter.new(options[:io], options) end + + mattr_accessor(:run_with_autorun) { false } + mattr_accessor(:run_with_rails_extension) { false } end + +Minitest.load_plugins Minitest.extensions << 'rails' diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 64e99626eb..09b8675cf8 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -1,9 +1,13 @@ +require "active_support/core_ext/class/attribute" require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter + class_attribute :executable + self.executable = "bin/rails test" + def report - return if results.empty? + return if filtered_results.empty? io.puts io.puts "Failed tests:" io.puts @@ -11,12 +15,22 @@ module Rails end def aggregated_results # :nodoc: - filtered_results = results.dup - filtered_results.reject!(&:skipped?) unless options[:verbose] filtered_results.map do |result| location, line = result.method(result.name).source_location - "bin/rails test #{location}:#{line}" + "#{self.executable} #{relative_path_for(location)}:#{line}" end.join "\n" end + + def filtered_results + if options[:verbose] + results + else + results.reject(&:skipped?) + end + end + + def relative_path_for(file) + file.sub(/^#{Rails.root}\/?/, '') + end end end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb deleted file mode 100644 index 5573fa6904..0000000000 --- a/railties/lib/rails/test_unit/runner.rb +++ /dev/null @@ -1,137 +0,0 @@ -require "optparse" -require "rake/file_list" -require "method_source" - -module Rails - class TestRunner - class Options - def self.parse(args) - options = { backtrace: !ENV["BACKTRACE"].nil?, name: nil, environment: "test" } - - opt_parser = ::OptionParser.new do |opts| - opts.banner = "Usage: bin/rails test [options] [file or directory]" - - opts.separator "" - opts.on("-e", "--environment [ENV]", - "Run tests in the ENV environment") do |env| - options[:environment] = env.strip - end - opts.separator "" - opts.separator "Filter options:" - opts.separator "" - opts.separator <<-DESC - You can run a single test by appending the line number to filename: - - bin/rails test test/models/user_test.rb:27 - - DESC - - opts.on("-n", "--name [NAME]", - "Only run tests matching NAME") do |name| - options[:name] = name - end - opts.on("-p", "--pattern [PATTERN]", - "Only run tests matching PATTERN") do |pattern| - options[:name] = "/#{pattern}/" - end - - opts.separator "" - opts.separator "Output options:" - - opts.on("-b", "--backtrace", - "Show the complete backtrace") do - options[:backtrace] = true - end - - opts.separator "" - opts.separator "Common options:" - - opts.on_tail("-h", "--help", "Show this message") do - puts opts - exit - end - end - - opt_parser.order!(args) - - options[:patterns] = [] - while arg = args.shift - if (file_and_line = arg.split(':')).size > 1 - options[:filename], options[:line] = file_and_line - options[:filename] = File.expand_path options[:filename] - options[:line] &&= options[:line].to_i - else - arg = arg.gsub(':', '') - if Dir.exist?("#{arg}") - options[:patterns] << File.expand_path("#{arg}/**/*_test.rb") - elsif File.file?(arg) - options[:patterns] << File.expand_path(arg) - end - end - end - options - end - end - - def initialize(options = {}) - @options = options - end - - def self.run(arguments) - options = Rails::TestRunner::Options.parse(arguments) - Rails::TestRunner.new(options).run - end - - def run - $rails_test_runner = self - ENV["RAILS_ENV"] = @options[:environment] - run_tests - end - - def find_method - return @options[:name] if @options[:name] - return unless @options[:line] - method = test_methods.find do |location, test_method, start_line, end_line| - location == @options[:filename] && - (start_line..end_line).include?(@options[:line].to_i) - end - method[1] if method - end - - def show_backtrace? - @options[:backtrace] - end - - def test_files - return [@options[:filename]] if @options[:filename] - if @options[:patterns] && @options[:patterns].count > 0 - pattern = @options[:patterns] - else - pattern = "test/**/*_test.rb" - end - Rake::FileList[pattern] - end - - private - def run_tests - test_files.to_a.each do |file| - require File.expand_path file - end - end - - def test_methods - methods_map = [] - suites = Minitest::Runnable.runnables.shuffle - suites.each do |suite_class| - suite_class.runnable_methods.each do |test_method| - method = suite_class.instance_method(test_method) - location = method.source_location - start_line = location.last - end_line = method.source.split("\n").size + start_line - 1 - methods_map << [File.expand_path(location.first), test_method, start_line, end_line] - end - end - methods_map - end - end -end diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb new file mode 100644 index 0000000000..84c2256729 --- /dev/null +++ b/railties/lib/rails/test_unit/test_requirer.rb @@ -0,0 +1,28 @@ +require 'active_support/core_ext/object/blank' +require 'rake/file_list' + +module Rails + class TestRequirer # :nodoc: + class << self + def require_files(patterns) + patterns = expand_patterns(patterns) + + Rake::FileList[patterns.compact.presence || 'test/**/*_test.rb'].to_a.each do |file| + require File.expand_path(file) + end + end + + private + def expand_patterns(patterns) + patterns.map do |arg| + arg = arg.gsub(/:(\d+)?$/, '') + if Dir.exist?(arg) + "#{arg}/**/*_test.rb" + elsif File.file?(arg) + arg + end + end + end + end + end +end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 0f26621b59..dda492f974 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,12 +1,13 @@ -require "rails/test_unit/runner" +gem 'minitest' +require 'minitest' +require 'rails/test_unit/minitest_plugin' task default: :test desc "Runs all tests in test folder" task :test do $: << "test" - args = ARGV[0] == "test" ? ARGV[1..-1] : [] - Rails::TestRunner.run(args) + Minitest.run(['test']) end namespace :test do @@ -23,22 +24,22 @@ namespace :test do ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| task name => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/#{name}"]) + Minitest.run(["test/#{name}"]) end end task :generators => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/lib/generators"]) + Minitest.run(["test/lib/generators"]) end task :units => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/models", "test/helpers", "test/unit"]) + Minitest.run(["test/models", "test/helpers", "test/unit"]) end task :functionals => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/controllers", "test/mailers", "test/functional"]) + Minitest.run(["test/controllers", "test/mailers", "test/functional"]) end end diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index acd387256c..36ab8109a7 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -36,7 +36,7 @@ module ApplicationTests test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do # config.assets.debug and config.assets.compile are false for production environment ENV["RAILS_ENV"] = "production" - output = Dir.chdir(app_path){ `bundle exec rake assets:precompile --trace 2>&1` } + output = Dir.chdir(app_path){ `bin/rake assets:precompile --trace 2>&1` } assert $?.success?, output require "#{app_path}/config/environment" diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 1e2a9ba040..ba2c27d8e8 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -18,7 +18,7 @@ module ApplicationTests def precompile!(env = nil) quietly do - precompile_task = "bundle exec rake assets:precompile #{env} --trace 2>&1" + precompile_task = "bin/rake assets:precompile #{env} --trace 2>&1" output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } assert $?.success?, output output @@ -27,7 +27,7 @@ module ApplicationTests def clean_assets! quietly do - assert Dir.chdir(app_path) { system('bundle exec rake assets:clobber') } + assert Dir.chdir(app_path) { system('bin/rake assets:clobber') } end end @@ -60,6 +60,7 @@ module ApplicationTests test "assets do not require compressors until it is used" do app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" add_to_env_config "production", "config.assets.compile = true" + add_to_env_config "production", "config.assets.precompile = []" ENV["RAILS_ENV"] = "production" require "#{app_path}/config/environment" diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 38516a1c1a..f677a7c42a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -745,6 +745,19 @@ module ApplicationTests assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") end + test "allows setting the queue name for the ActionMailer::DeliveryJob" do + add_to_config <<-RUBY + config.action_mailer.deliver_later_queue_name = 'test_default' + RUBY + + require "#{app_path}/config/environment" + require "mail" + + _ = ActionMailer::Base + + assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name") + end + test "valid timezone is setup correctly" do add_to_config <<-RUBY config.root = "#{app_path}" diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 31bc003dcb..7bf123d12b 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -29,6 +29,18 @@ class ConsoleTest < ActiveSupport::TestCase assert_instance_of ActionDispatch::Integration::Session, console_session end + def test_app_can_access_path_helper_method + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get 'foo', to: 'foo#index' + end + RUBY + + load_environment + console_session = irb_context.app + assert_equal '/foo', console_session.foo_path + end + def test_new_session_should_return_integration_session load_environment session = irb_context.new_session diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index 78ada58ec8..84cc6e120b 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -125,5 +125,40 @@ module ApplicationTests assert_equal expected, c.generators.options end end + + test "api only generators hide assets, helper, js and css namespaces and set api option" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + # Initialize the application + require "#{app_path}/config/environment" + Rails.application.load_generators + + assert Rails::Generators.hidden_namespaces.include?("assets") + assert Rails::Generators.hidden_namespaces.include?("helper") + assert Rails::Generators.hidden_namespaces.include?("js") + assert Rails::Generators.hidden_namespaces.include?("css") + assert Rails::Generators.options[:rails][:api] + assert_equal false, Rails::Generators.options[:rails][:assets] + assert_equal false, Rails::Generators.options[:rails][:helper] + assert_nil Rails::Generators.options[:rails][:template_engine] + end + + test "api only generators allow overriding generator options" do + add_to_config <<-RUBY + config.generators.helper = true + config.api_only = true + config.generators.template_engine = :my_template + RUBY + + # Initialize the application + require "#{app_path}/config/environment" + Rails.application.load_generators + + assert Rails::Generators.options[:rails][:api] + assert Rails::Generators.options[:rails][:helper] + assert_equal :my_template, Rails::Generators.options[:rails][:template_engine] + end end end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 97b51911d9..7bba910b9e 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -129,6 +129,35 @@ module ApplicationTests assert_equal "false", last_response.body end + test "action_controller api executes using all the middleware stack" do + add_to_config "config.api_only = true" + + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::API + end + RUBY + + app_file "app/controllers/omg_controller.rb", <<-RUBY + class OmgController < ApplicationController + def show + render json: { omg: 'omg' } + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + + get 'omg/show' + assert_equal '{"omg":"omg"}', last_response.body + end + # AD test "action_dispatch extensions are applied to ActionDispatch" do add_to_config "config.action_dispatch.tld_length = 2" @@ -177,7 +206,7 @@ module ApplicationTests test "use schema cache dump" do Dir.chdir(app_path) do `rails generate model post title:string; - bundle exec rake db:migrate db:schema:cache:dump` + bin/rake db:migrate db:schema:cache:dump` end require "#{app_path}/config/environment" ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test. @@ -187,7 +216,7 @@ module ApplicationTests test "expire schema cache dump" do Dir.chdir(app_path) do `rails generate model post title:string; - bundle exec rake db:migrate db:schema:cache:dump db:rollback` + bin/rake db:migrate db:schema:cache:dump db:rollback` end silence_warnings { require "#{app_path}/config/environment" diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 9ee54796a4..ab7f29b0f2 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -132,6 +132,79 @@ en: assert_equal "2", last_response.body end + test "new locale files are loaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + load_app + + get "/i18n" + assert_equal "1", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + remove_file "config/locales/en.yml" + app_file "config/locales/custom.en.yml", <<-YAML +en: + foo: "2" + YAML + + get "/i18n" + assert_equal "2", last_response.body + end + + test "I18n.load_path is reloaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.load_path.inspect]] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + load_app + + get "/i18n" + + assert_match "en.yml", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + app_file "config/locales/fr.yml", <<-YAML +fr: + foo: "2" + YAML + + get "/i18n" + assert_match "fr.yml", last_response.body + assert_match "en.yml", last_response.body + end + # Fallbacks test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do I18n.backend = Class.new(I18n::Backend::Simple).new diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 04bd19784a..d298e8d632 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -26,7 +26,7 @@ module ApplicationTests assert_equal [ "Rack::Sendfile", "ActionDispatch::Static", - "Rack::Lock", + "ActionDispatch::LoadInterlock", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "Rack::MethodOverride", @@ -50,6 +50,33 @@ module ApplicationTests ], middleware end + test "api middleware stack" do + add_to_config "config.api_only = true" + + boot! + + assert_equal [ + "Rack::Sendfile", + "ActionDispatch::Static", + "ActionDispatch::LoadInterlock", + "ActiveSupport::Cache::Strategy::LocalCache", + "Rack::Runtime", + "ActionDispatch::RequestId", + "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::ShowExceptions", + "ActionDispatch::DebugExceptions", + "ActionDispatch::RemoteIp", + "ActionDispatch::Reloader", + "ActionDispatch::Callbacks", + "ActiveRecord::ConnectionAdapters::ConnectionManagement", + "ActiveRecord::QueryCache", + "ActionDispatch::ParamsParser", + "Rack::Head", + "Rack::ConditionalGet", + "Rack::ETag" + ], middleware + end + test "Rack::Cache is not included by default" do boot! @@ -94,23 +121,40 @@ module ApplicationTests assert !middleware.include?("ActiveRecord::Migration::CheckPending") end - test "includes lock if cache_classes is set but eager_load is not" do + test "includes interlock if cache_classes is set but eager_load is not" do add_to_config "config.cache_classes = true" boot! - assert middleware.include?("Rack::Lock") + assert_not_includes middleware, "Rack::Lock" + assert_includes middleware, "ActionDispatch::LoadInterlock" + end + + test "includes interlock if cache_classes is off" do + add_to_config "config.cache_classes = false" + boot! + assert_not_includes middleware, "Rack::Lock" + assert_includes middleware, "ActionDispatch::LoadInterlock" end test "does not include lock if cache_classes is set and so is eager_load" do add_to_config "config.cache_classes = true" add_to_config "config.eager_load = true" boot! - assert !middleware.include?("Rack::Lock") + assert_not_includes middleware, "Rack::Lock" + assert_not_includes middleware, "ActionDispatch::LoadInterlock" + end + + test "does not include lock if allow_concurrency is set to :unsafe" do + add_to_config "config.allow_concurrency = :unsafe" + boot! + assert_not_includes middleware, "Rack::Lock" + assert_not_includes middleware, "ActionDispatch::LoadInterlock" end - test "does not include lock if allow_concurrency is set" do - add_to_config "config.allow_concurrency = true" + test "includes lock if allow_concurrency is disabled" do + add_to_config "config.allow_concurrency = false" boot! - assert !middleware.include?("Rack::Lock") + assert_includes middleware, "Rack::Lock" + assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "removes static asset server if serve_static_files is disabled" do diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index cddc79cc85..f2770a9cb4 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -118,7 +118,7 @@ module ApplicationTests assert_equal 0, run_count, "Without loading the initializers, the count should be 0" # Set config.eager_load to false so that an eager_load warning doesn't pop up - AppTemplate::Application.new { config.eager_load = false }.initialize! + AppTemplate::Application.create { config.eager_load = false }.initialize! assert_equal 3, run_count, "There should have been three initializers that incremented the count" end diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb new file mode 100644 index 0000000000..3198e12662 --- /dev/null +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -0,0 +1,63 @@ +require 'isolation/abstract_unit' +require 'rack/test' +require 'minitest/mock' + +require 'action_view' +require 'active_support/testing/method_call_assertions' + +class PerRequestDigestCacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::MethodCallAssertions + include Rack::Test::Methods + + setup do + build_app + add_to_config 'config.consider_all_requests_local = true' + + app_file 'app/models/customer.rb', <<-RUBY + class Customer < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + resources :customers, only: :index + end + RUBY + + app_file 'app/controllers/customers_controller.rb', <<-RUBY + class CustomersController < ApplicationController + def index + render [ Customer.new('david', 1), Customer.new('dingus', 2) ] + end + end + RUBY + + app_file 'app/views/customers/_customer.html.erb', <<-RUBY + <% cache customer do %> + <%= customer.name %> + <% end %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "digests are reused when rendering the same template twice" do + get '/customers' + assert_equal 200, last_response.status + + assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], ActionView::Digestor.cache.values + assert_equal %w(david dingus), last_response.body.split.map(&:strip) + end + + test "template digests are cleared before a request" do + assert_called(ActionView::Digestor.cache, :clear) do + get '/customers' + assert_equal 200, last_response.status + end + end +end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 5cc9790b28..e7beab8b5e 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -28,11 +28,11 @@ module ApplicationTests def db_create_and_drop(expected_database) Dir.chdir(app_path) do - output = `bundle exec rake db:create` + output = `bin/rake db:create` assert_empty output assert File.exist?(expected_database) assert_equal expected_database, ActiveRecord::Base.connection_config[:database] - output = `bundle exec rake db:drop` + output = `bin/rake db:drop` assert_empty output assert !File.exist?(expected_database) end @@ -51,9 +51,9 @@ module ApplicationTests def db_migrate_and_status(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` - output = `bundle exec rake db:migrate:status` + `bin/rails generate model book title:string; + bin/rake db:migrate` + output = `bin/rake db:migrate:status` assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output) assert_match(/up\s+\d{14}\s+Create books/, output) end @@ -72,8 +72,8 @@ module ApplicationTests def db_schema_dump Dir.chdir(app_path) do - `rails generate model book title:string; - rake db:migrate db:schema:dump` + `bin/rails generate model book title:string; + bin/rake db:migrate db:schema:dump` schema_dump = File.read("db/schema.rb") assert_match(/create_table \"books\"/, schema_dump) end @@ -90,8 +90,8 @@ module ApplicationTests def db_fixtures_load(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:fixtures:load` + `bin/rails generate model book title:string; + bin/rake db:migrate db:fixtures:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" assert_equal 2, Book.count @@ -112,8 +112,8 @@ module ApplicationTests test 'db:fixtures:load with namespaced fixture' do require "#{app_path}/config/environment" Dir.chdir(app_path) do - `rails generate model admin::book title:string; - bundle exec rake db:migrate db:fixtures:load` + `bin/rails generate model admin::book title:string; + bin/rake db:migrate db:fixtures:load` require "#{app_path}/app/models/admin/book" assert_equal 2, Admin::Book.count end @@ -121,11 +121,11 @@ module ApplicationTests def db_structure_dump_and_load(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:structure:dump` + `bin/rails generate model book title:string; + bin/rake db:migrate db:structure:dump` structure_dump = File.read("db/structure.sql") assert_match(/CREATE TABLE \"books\"/, structure_dump) - `bundle exec rake environment db:drop db:structure:load` + `bin/rake environment db:drop db:structure:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised @@ -147,9 +147,9 @@ module ApplicationTests test 'db:structure:dump does not dump schema information when no migrations are used' do Dir.chdir(app_path) do # create table without migrations - `bundle exec rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` + `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` - stderr_output = capture(:stderr) { `bundle exec rake db:structure:dump` } + stderr_output = capture(:stderr) { `bin/rake db:structure:dump` } assert_empty stderr_output structure_dump = File.read("db/structure.sql") assert_match(/CREATE TABLE \"posts\"/, structure_dump) @@ -211,8 +211,8 @@ module ApplicationTests def db_test_load_structure Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:structure:dump db:test:load_structure` + `bin/rails generate model book title:string; + bin/rake db:migrate db:structure:dump db:test:load_structure` ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" @@ -248,7 +248,7 @@ module ApplicationTests RUBY Dir.chdir(app_path) do - database_path = `bundle exec rake db:setup` + database_path = `bin/rake db:setup` assert_equal "development.sqlite3", File.basename(database_path.strip) end ensure diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb new file mode 100644 index 0000000000..28d8b22a37 --- /dev/null +++ b/railties/test/application/rake/dev_test.rb @@ -0,0 +1,35 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + module RakeTests + class RakeDevTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + test 'dev:cache creates file and outputs message' do + Dir.chdir(app_path) do + output = `rake dev:cache` + assert File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is now being cached/, output) + end + end + + test 'dev:cache deletes file and outputs message' do + Dir.chdir(app_path) do + output = `rake dev:cache` + output = `rake dev:cache` + assert_not File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is no longer being cached/, output) + end + end + end + end +end diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index a3819b93b2..2d8bd7c571 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -15,21 +15,21 @@ module ApplicationTests test 'running migrations with given scope' do Dir.chdir(app_path) do - `rails generate model user username:string password:string` + `bin/rails generate model user username:string password:string` app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION class AMigration < ActiveRecord::Migration end MIGRATION - output = `rake db:migrate SCOPE=bukkits` + output = `bin/rake db:migrate SCOPE=bukkits` assert_no_match(/create_table\(:users\)/, output) assert_no_match(/CreateUsers/, output) assert_no_match(/add_column\(:users, :email, :string\)/, output) assert_match(/AMigration: migrated/, output) - output = `rake db:migrate SCOPE=bukkits VERSION=0` + output = `bin/rake db:migrate SCOPE=bukkits VERSION=0` assert_no_match(/drop_table\(:users\)/, output) assert_no_match(/CreateUsers/, output) assert_no_match(/remove_column\(:users, :email\)/, output) @@ -40,16 +40,16 @@ module ApplicationTests test 'model and migration generator with change syntax' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string` - output = `rake db:migrate` + output = `bin/rake db:migrate` assert_match(/create_table\(:users\)/, output) assert_match(/CreateUsers: migrated/, output) assert_match(/add_column\(:users, :email, :string\)/, output) assert_match(/AddEmailToUsers: migrated/, output) - output = `rake db:rollback STEP=2` + output = `bin/rake db:rollback STEP=2` assert_match(/drop_table\(:users\)/, output) assert_match(/CreateUsers: reverted/, output) assert_match(/remove_column\(:users, :email, :string\)/, output) @@ -58,23 +58,23 @@ module ApplicationTests end test 'migration status when schema migrations table is not present' do - output = Dir.chdir(app_path){ `rake db:migrate:status 2>&1` } + output = Dir.chdir(app_path){ `bin/rake db:migrate:status 2>&1` } assert_equal "Schema migrations table does not exist yet.\n", output end test 'test migration status' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rake db:migrate` - output = `rake db:migrate:status` + output = `bin/rake 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) - `rake db:rollback STEP=1` - output = `rake db:migrate:status` + `bin/rake db:rollback STEP=1` + output = `bin/rake db:migrate:status` assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/down\s+\d{14}\s+Add email to users/, output) @@ -85,17 +85,17 @@ module ApplicationTests add_to_config('config.active_record.timestamped_migrations = false') Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rake db:migrate` - output = `rake db:migrate:status` + output = `bin/rake db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) - `rake db:rollback STEP=1` - output = `rake db:migrate:status` + `bin/rake db:rollback STEP=1` + output = `bin/rake db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/down\s+\d{3,}\s+Add email to users/, output) @@ -104,23 +104,23 @@ module ApplicationTests test 'test migration status after rollback and redo' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rake db:migrate` - output = `rake db:migrate:status` + output = `bin/rake 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) - `rake db:rollback STEP=2` - output = `rake db:migrate:status` + `bin/rake db:rollback STEP=2` + output = `bin/rake 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) - `rake db:migrate:redo` - output = `rake db:migrate:status` + `bin/rake db:migrate:redo` + output = `bin/rake 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) @@ -131,23 +131,23 @@ module ApplicationTests add_to_config('config.active_record.timestamped_migrations = false') Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rake db:migrate` - output = `rake db:migrate:status` + output = `bin/rake db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) - `rake db:rollback STEP=2` - output = `rake db:migrate:status` + `bin/rake db:rollback STEP=2` + output = `bin/rake db:migrate:status` assert_match(/down\s+\d{3,}\s+Create users/, output) assert_match(/down\s+\d{3,}\s+Add email to users/, output) - `rake db:migrate:redo` - output = `rake db:migrate:status` + `bin/rake db:migrate:redo` + output = `bin/rake db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) @@ -158,27 +158,29 @@ module ApplicationTests add_to_config('config.active_record.dump_schema_after_migration = false') Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` + `bin/rails generate model book title:string` + output = `bin/rails generate model author name:string` + version = output =~ %r{[^/]+db/migrate/(\d+)_create_authors\.rb} && $1 - assert !File.exist?("db/schema.rb") + `bin/rake db:migrate db:rollback db:forward db:migrate:up db:migrate:down VERSION=#{version}` + assert !File.exist?("db/schema.rb"), "should not dump schema when configured not to" end add_to_config('config.active_record.dump_schema_after_migration = true') Dir.chdir(app_path) do - `rails generate model author name:string; - bundle exec rake db:migrate` + `bin/rails generate model reviews book_id:integer` + `bin/rake db:migrate` structure_dump = File.read("db/schema.rb") - assert_match(/create_table "authors"/, structure_dump) + assert_match(/create_table "reviews"/, structure_dump) end end test 'default schema generation after migration' do Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` + `bin/rails generate model book title:string; + bin/rake db:migrate` structure_dump = File.read("db/schema.rb") assert_match(/create_table "books"/, structure_dump) @@ -187,12 +189,12 @@ module ApplicationTests test 'test migration status migrated file is deleted' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rake db:migrate rm db/migrate/*email*.rb` - output = `rake db:migrate:status` + output = `bin/rake db:migrate:status` File.write('test.txt', output) assert_match(/up\s+\d{14}\s+Create users/, output) diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 95087bf29f..c87515f00f 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -74,7 +74,7 @@ module ApplicationTests app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" - run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes" do |output, lines| + run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rake notes" do |output, lines| assert_match(/note in app directory/, output) assert_match(/note in config directory/, output) assert_match(/note in db directory/, output) @@ -102,7 +102,7 @@ module ApplicationTests end EOS - run_rake_notes "bundle exec rake notes_custom" do |output, lines| + run_rake_notes "bin/rake notes_custom" do |output, lines| assert_match(/\[FIXME\] note in lib directory/, output) assert_match(/\[TODO\] note in test directory/, output) assert_no_match(/OPTIMIZE/, output) @@ -114,6 +114,7 @@ module ApplicationTests end test 'register a new extension' do + add_to_config "config.assets.precompile = []" add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } } app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" @@ -127,7 +128,7 @@ module ApplicationTests private - def run_rake_notes(command = 'bundle exec rake notes') + def run_rake_notes(command = 'bin/rake notes') boot_rails load_tasks diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb index 35099913fb..4cae199e6b 100644 --- a/railties/test/application/rake/restart_test.rb +++ b/railties/test/application/rake/restart_test.rb @@ -13,12 +13,12 @@ module ApplicationTests def teardown teardown_app end - + test 'rake restart touches tmp/restart.txt' do Dir.chdir(app_path) do `rake restart` assert File.exist?("tmp/restart.txt") - + prev_mtime = File.mtime("tmp/restart.txt") sleep(1) `rake restart` @@ -26,6 +26,14 @@ module ApplicationTests assert_not_equal prev_mtime, curr_mtime end end + + test 'rake restart should work even if tmp folder does not exist' do + Dir.chdir(app_path) do + FileUtils.remove_dir('tmp') + `rake restart` + assert File.exist?('tmp/restart.txt') + end + end end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index de14f269df..a040dd4cf6 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -36,7 +36,7 @@ module ApplicationTests Rails.application.initialize! RUBY - assert_match("SuperMiddleware", Dir.chdir(app_path){ `rake middleware` }) + assert_match("SuperMiddleware", Dir.chdir(app_path){ `bin/rake middleware` }) end def test_initializers_are_executed_in_rake_tasks @@ -51,7 +51,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `rake do_nothing` } + output = Dir.chdir(app_path){ `bin/rake do_nothing` } assert_match "Doing something...", output end @@ -72,7 +72,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path) { `rake do_nothing` } + output = Dir.chdir(app_path) { `bin/rake do_nothing` } assert_match 'Hello world', output end @@ -93,14 +93,14 @@ module ApplicationTests RUBY Dir.chdir(app_path) do - assert system('rake do_nothing RAILS_ENV=production'), + assert system('bin/rake do_nothing RAILS_ENV=production'), 'should not be pre-required for rake even eager_load=true' end end def test_code_statistics_sanity assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0", - Dir.chdir(app_path){ `rake stats` } + Dir.chdir(app_path){ `bin/rake stats` } end def test_rake_routes_calls_the_route_inspector @@ -110,7 +110,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `rake routes` } + output = Dir.chdir(app_path){ `bin/rake routes` } assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end @@ -123,7 +123,7 @@ module ApplicationTests RUBY ENV['CONTROLLER'] = 'cart' - output = Dir.chdir(app_path){ `rake routes` } + output = Dir.chdir(app_path){ `bin/rake routes` } assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end @@ -133,7 +133,7 @@ module ApplicationTests end RUBY - assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `rake routes` } + assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `bin/rake routes` } You don't have any routes defined! Please add some routes in config/routes.rb. @@ -151,21 +151,21 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `rake log_something RAILS_ENV=production && cat log/production.log` } + output = Dir.chdir(app_path){ `bin/rake log_something RAILS_ENV=production && cat log/production.log` } assert_match "Sample log message", output end def test_loading_specific_fixtures Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate model product name:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate model product name:string; + bin/rake db:migrate` end require "#{rails_root}/config/environment" # loading a specific fixture - errormsg = Dir.chdir(app_path) { `rake db:fixtures:load FIXTURES=products` } + errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load FIXTURES=products` } assert $?.success?, errormsg assert_equal 2, ::AppTemplate::Application::Product.count @@ -174,33 +174,52 @@ module ApplicationTests def test_loading_only_yml_fixtures Dir.chdir(app_path) do - `rake db:migrate` + `bin/rake db:migrate` end app_file "test/fixtures/products.csv", "" require "#{rails_root}/config/environment" - errormsg = Dir.chdir(app_path) { `rake db:fixtures:load` } + errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load` } assert $?.success?, errormsg end def test_scaffold_tests_pass_by_default output = Dir.chdir(app_path) do - `rails generate scaffold user username:string password:string; - bundle exec rake db:migrate test` + `bin/rails generate scaffold user username:string password:string; + bin/rake db:migrate test` end assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end + def test_api_scaffold_tests_pass_by_default + add_to_config <<-RUBY + config.api_only = true + RUBY + + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::API + end + RUBY + + output = Dir.chdir(app_path) do + `bin/rails generate scaffold user username:string password:string; + bin/rake db:migrate test` + end + + assert_match(/5 runs, 7 assertions, 0 failures, 0 errors/, output) + assert_no_match(/Errors running/, output) + end + def test_scaffold_with_references_columns_tests_pass_when_belongs_to_is_optional app_file "config/initializers/active_record_belongs_to_required_by_default.rb", "Rails.application.config.active_record.belongs_to_required_by_default = false" output = Dir.chdir(app_path) do - `rails generate scaffold LineItems product:references cart:belongs_to; - bundle exec rake db:migrate test` + `bin/rails generate scaffold LineItems product:references cart:belongs_to; + bin/rake db:migrate test` end assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) @@ -210,9 +229,9 @@ module ApplicationTests def test_db_test_clone_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails generate scaffold user username:string; - bundle exec rake db:migrate; - bundle exec rake db:test:clone 2>&1 --trace` + `bin/rails generate scaffold user username:string; + bin/rake db:migrate; + bin/rake db:test:clone 2>&1 --trace` end assert_match(/Execute db:test:clone_structure/, output) end @@ -220,9 +239,9 @@ module ApplicationTests def test_db_test_prepare_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails generate scaffold user username:string; - bundle exec rake db:migrate; - bundle exec rake db:test:prepare 2>&1 --trace` + `bin/rails generate scaffold user username:string; + bin/rake db:migrate; + bin/rake db:test:prepare 2>&1 --trace` end assert_match(/Execute db:test:load_structure/, output) end @@ -230,7 +249,7 @@ module ApplicationTests def test_rake_dump_structure_should_respect_db_structure_env_variable Dir.chdir(app_path) do # ensure we have a schema_migrations table to dump - `bundle exec rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql` + `bin/rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql` end assert File.exist?(File.join(app_path, 'db', 'my_structure.sql')) end @@ -239,8 +258,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails g model post title:string; - bundle exec rake db:migrate:redo 2>&1 --trace;` + `bin/rails g model post title:string; + bin/rake db:migrate:redo 2>&1 --trace;` end # expect only Invoke db:structure:dump (first_time) @@ -249,23 +268,23 @@ module ApplicationTests def test_rake_dump_schema_cache Dir.chdir(app_path) do - `rails generate model post title:string; - rails generate model product name:string; - bundle exec rake db:migrate db:schema:cache:dump` + `bin/rails generate model post title:string; + bin/rails generate model product name:string; + bin/rake db:migrate db:schema:cache:dump` end assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_rake_clear_schema_cache Dir.chdir(app_path) do - `bundle exec rake db:schema:cache:dump db:schema:cache:clear` + `bin/rake db:schema:cache:dump db:schema:cache:clear` end assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_copy_templates Dir.chdir(app_path) do - `bundle exec rake rails:templates:copy` + `bin/rake rails:templates:copy` %w(controller mailer scaffold).each do |dir| assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir)) end @@ -280,10 +299,17 @@ module ApplicationTests app_file "template.rb", "" output = Dir.chdir(app_path) do - `bundle exec rake rails:template LOCATION=template.rb` + `bin/rake rails:template LOCATION=template.rb` end assert_match(/Hello, World!/, output) end + + def test_tmp_clear_should_work_if_folder_missing + FileUtils.remove_dir("#{app_path}/tmp") + errormsg = Dir.chdir(app_path) { `bin/rake tmp:clear` } + assert_predicate $?, :success? + assert_empty errormsg + end end end diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index 6595c40f8b..0c180339b4 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -25,15 +25,15 @@ module ApplicationTests end def test_should_include_runner_in_shebang_line_in_help_without_option - assert_match "/rails runner", Dir.chdir(app_path) { `bundle exec rails runner` } + assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner` } end def test_should_include_runner_in_shebang_line_in_help - assert_match "/rails runner", Dir.chdir(app_path) { `bundle exec rails runner --help` } + assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner --help` } end def test_should_run_ruby_statement - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "puts User.count"` } + assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` } end def test_should_run_file @@ -41,7 +41,7 @@ module ApplicationTests puts User.count SCRIPT - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "bin/count_users.rb"` } + assert_match "42", Dir.chdir(app_path) { `bin/rails runner "bin/count_users.rb"` } end def test_should_set_dollar_0_to_file @@ -49,7 +49,7 @@ module ApplicationTests puts $0 SCRIPT - assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/dollar0.rb"` } + assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bin/rails runner "bin/dollar0.rb"` } end def test_should_set_dollar_program_name_to_file @@ -57,7 +57,7 @@ module ApplicationTests puts $PROGRAM_NAME SCRIPT - assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/program_name.rb"` } + assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb"` } end def test_with_hook @@ -67,22 +67,22 @@ module ApplicationTests end RUBY - assert_match "true", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.application.config.ran"` } + assert_match "true", Dir.chdir(app_path) { `bin/rails runner "puts Rails.application.config.ran"` } end def test_default_environment - assert_match "development", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "development", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } end def test_environment_with_rails_env with_rails_env "production" do - assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } end end def test_environment_with_rack_env with_rack_env "production" do - assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } end end end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index c122b315c0..494e6dd7bd 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -1,9 +1,10 @@ require 'isolation/abstract_unit' require 'active_support/core_ext/string/strip' +require 'env_helpers' module ApplicationTests class TestRunnerTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::Isolation, EnvHelpers def setup build_app @@ -14,20 +15,6 @@ module ApplicationTests teardown_app end - def test_run_in_test_environment - app_file 'test/unit/env_test.rb', <<-RUBY - require 'test_helper' - - class EnvTest < ActiveSupport::TestCase - def test_env - puts "Current Environment: \#{Rails.env}" - end - end - RUBY - - assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') - end - def test_run_single_file create_test_file :models, 'foo' create_test_file :models, 'bar' @@ -187,7 +174,7 @@ module ApplicationTests end RUBY - run_test_command('-p rikka test/unit/chu_2_koi_test.rb').tap do |output| + run_test_command('-n /rikka/ test/unit/chu_2_koi_test.rb').tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -229,24 +216,128 @@ module ApplicationTests assert_match "development", run_test_command('test/unit/env_test.rb') end + def test_run_in_test_environment_by_default + create_env_test + + assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') + end + def test_run_different_environment - env = "development" - app_file 'test/unit/env_test.rb', <<-RUBY + create_env_test + + assert_match "Current Environment: development", + run_test_command("-e development test/unit/env_test.rb") + end + + def test_generated_scaffold_works_with_rails_test + create_scaffold + assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + end + + def test_run_multiple_folders + create_test_file :models, 'account' + create_test_file :controllers, 'accounts_controller' + + run_test_command('test/models test/controllers').tap do |output| + assert_match 'AccountTest', output + assert_match 'AccountsControllerTest', output + assert_match '2 runs, 2 assertions, 0 failures, 0 errors, 0 skips', output + end + end + + def test_run_with_ruby_command + app_file 'test/models/post_test.rb', <<-RUBY require 'test_helper' - class EnvTest < ActiveSupport::TestCase - def test_env - puts Rails.env + class PostTest < ActiveSupport::TestCase + test 'declarative syntax works' do + puts 'PostTest' + assert true end end RUBY - assert_match env, run_test_command("-e #{env} test/unit/env_test.rb") + Dir.chdir(app_path) do + `ruby -Itest test/models/post_test.rb`.tap do |output| + assert_match 'PostTest', output + assert_no_match 'is already defined in', output + end + end end - def test_generated_scaffold_works_with_rails_test - create_scaffold - assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + def test_mix_files_and_line_filters + create_test_file :models, 'account' + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + puts 'PostTest' + assert true + end + + def test_line_filter_does_not_run_this + assert true + end + end + RUBY + + run_test_command('test/models/account_test.rb test/models/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_multiple_line_filters + create_test_file :models, 'account' + create_test_file :models, 'post' + + run_test_command('test/models/account_test.rb:4 test/models/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + end + end + + def test_line_filter_without_line_runs_all_tests + create_test_file :models, 'account' + + run_test_command('test/models/account_test.rb:').tap do |output| + assert_match 'AccountTest', output + end + end + + def test_shows_filtered_backtrace_by_default + create_backtrace_test + + assert_match 'Rails::BacktraceCleaner', run_test_command('test/unit/backtrace_test.rb') + end + + def test_backtrace_option + create_backtrace_test + + assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb -b') + assert_match 'Minitest::BacktraceFilter', + run_test_command('test/unit/backtrace_test.rb --backtrace') + end + + def test_show_full_backtrace_using_backtrace_environment_variable + create_backtrace_test + + switch_env 'BACKTRACE', 'true' do + assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb') + end + end + + 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__) + + require 'bundler/setup' # Set up gems listed in the Gemfile. + RUBY + + assert_match '0 runs, 0 assertions', run_test_command('') end private @@ -284,6 +375,18 @@ module ApplicationTests RUBY end + def create_backtrace_test + app_file 'test/unit/backtrace_test.rb', <<-RUBY + require 'test_helper' + + class BacktraceTest < ActiveSupport::TestCase + def test_backtrace + puts Minitest.backtrace_filter + end + end + RUBY + end + def create_schema app_file 'db/schema.rb', '' end @@ -301,6 +404,18 @@ module ApplicationTests RUBY end + def create_env_test + app_file 'test/unit/env_test.rb', <<-RUBY + require 'test_helper' + + class EnvTest < ActiveSupport::TestCase + def test_env + puts "Current Environment: \#{Rails.env}" + end + end + RUBY + end + def create_scaffold script 'generate scaffold user name:string' Dir.chdir(app_path) { File.exist?('app/models/user.rb') } @@ -308,7 +423,7 @@ module ApplicationTests end def run_migration - Dir.chdir(app_path) { `bundle exec rake db:migrate` } + Dir.chdir(app_path) { `bin/rake db:migrate` } end end end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index 61652e5052..0e997f4ba7 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -44,7 +44,7 @@ module ApplicationTests def test_index get '/posts' assert_response :success - assert_template "index" + assert_includes @response.body, 'Posts#index' end end RUBY @@ -64,8 +64,8 @@ module ApplicationTests RUBY output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" }) - assert_match %r{/app/test/unit/failing_test\.rb}, output - assert_match %r{/app/test/unit/failing_test\.rb:4}, output + assert_match %r{test/unit/failing_test\.rb}, output + assert_match %r{test/unit/failing_test\.rb:4}, output end test "ruby schema migrations" do diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index ba688f1e9e..7a063aeddf 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -44,6 +44,22 @@ class Rails::ServerTest < ActiveSupport::TestCase end end + def test_caching_without_option + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal nil, options[:caching] + end + + def test_caching_with_option + args = ["--dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:caching] + + args = ["--no-dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:caching] + end + def test_log_stdout with_rack_env nil do with_rails_env nil do diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index c0b88089b3..2857dae07e 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,7 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'env_helpers' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' class ActionsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -12,11 +12,13 @@ class ActionsTest < Rails::Generators::TestCase def setup Rails.application = TestApp::Application + @mock_generator = Minitest::Mock.new super end def teardown Rails.application = TestApp::Application.instance + @mock_generator.verify end def test_invoke_other_generator_with_shortcut @@ -45,6 +47,14 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/ end + def test_add_source_with_block_adds_source_to_gemfile_with_gem + run_generator + action :add_source, 'http://gems.github.com' do + gem 'rspec-rails' + end + assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ + end + def test_gem_should_put_gem_dependency_in_gemfile run_generator action :gem, 'will-paginate' @@ -140,13 +150,18 @@ class ActionsTest < Rails::Generators::TestCase end def test_git_with_symbol_should_run_command_using_git_scm - generator.expects(:run).once.with('git init') - action :git, :init + @mock_generator.expect(:call, nil, ['git init']) + generator.stub(:run, @mock_generator) do + action :git, :init + end end def test_git_with_hash_should_run_each_command_using_git_scm - generator.expects(:run).times(2) - action :git, rm: 'README', add: '.' + @mock_generator.expect(:call, nil, ["git rm README"]) + @mock_generator.expect(:call, nil, ["git add ."]) + generator.stub(:run, @mock_generator) do + action :git, rm: 'README', add: '.' + end end def test_vendor_should_write_data_to_file_in_vendor @@ -170,46 +185,60 @@ class ActionsTest < Rails::Generators::TestCase end def test_generate_should_run_script_generate_with_argument_and_options - generator.expects(:run_ruby_script).once.with('bin/rails generate model MyModel', verbose: false) - action :generate, 'model', 'MyModel' + @mock_generator.expect(:call, nil, ['bin/rails generate model MyModel', verbose: false]) + generator.stub(:run_ruby_script, @mock_generator) do + action :generate, 'model', 'MyModel' + end end def test_rake_should_run_rake_command_with_default_env - generator.expects(:run).once.with("rake log:clear RAILS_ENV=development", verbose: false) - with_rails_env nil do - action :rake, 'log:clear' + @mock_generator.expect(:call, nil, ["rake log:clear RAILS_ENV=development", verbose: false]) + generator.stub(:run, @mock_generator) do + with_rails_env nil do + action :rake, 'log:clear' + end end end def test_rake_with_env_option_should_run_rake_command_in_env - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - action :rake, 'log:clear', env: 'production' + @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) + generator.stub(:run, @mock_generator) do + action :rake, 'log:clear', env: 'production' + end end def test_rake_with_rails_env_variable_should_run_rake_command_in_env - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - with_rails_env "production" do - action :rake, 'log:clear' + @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) + generator.stub(:run, @mock_generator) do + with_rails_env "production" do + action :rake, 'log:clear' + end end end def test_env_option_should_win_over_rails_env_variable_when_running_rake - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - with_rails_env "staging" do - action :rake, 'log:clear', env: 'production' + @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) + generator.stub(:run, @mock_generator) do + with_rails_env "staging" do + action :rake, 'log:clear', env: 'production' + end end end def test_rake_with_sudo_option_should_run_rake_command_with_sudo - generator.expects(:run).once.with("sudo rake log:clear RAILS_ENV=development", verbose: false) - with_rails_env nil do - action :rake, 'log:clear', sudo: true + @mock_generator.expect(:call, nil, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) + generator.stub(:run, @mock_generator) do + with_rails_env nil do + action :rake, 'log:clear', sudo: true + end end end def test_capify_should_run_the_capify_command - generator.expects(:run).once.with('capify .', verbose: false) - action :capify! + @mock_generator.expect(:call, nil, ['capify .', verbose: false]) + generator.stub(:run, @mock_generator) do + action :capify! + end end def test_route_should_add_data_to_the_routes_block_in_config_routes @@ -245,15 +274,19 @@ F def test_readme run_generator - Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_match "application up and running", action(:readme, "README.md") + 2.times { @mock_generator.expect(:call, destination_root,[]) } + Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do + assert_match "application up and running", action(:readme, "README.md") + end end def test_readme_with_quiet generator(default_arguments, quiet: true) run_generator - Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_no_match "application up and running", action(:readme, "README.md") + 2.times { @mock_generator.expect(:call, destination_root,[]) } + Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do + assert_no_match "application up and running", action(:readme, "README.md") + end end def test_log diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb new file mode 100644 index 0000000000..998da3ef84 --- /dev/null +++ b/railties/test/generators/api_app_generator_test.rb @@ -0,0 +1,97 @@ +require 'generators/generators_test_helper' +require 'rails/generators/rails/app/app_generator' + +class ApiAppGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::AppGenerator + + arguments [destination_root, '--api'] + + def setup + Rails.application = TestApp::Application + super + + Kernel::silence_warnings do + Thor::Base.shell.send(:attr_accessor, :always_force) + @shell = Thor::Base.shell.new + @shell.send(:always_force=, true) + end + end + + def teardown + super + Rails.application = TestApp::Application.instance + end + + def test_skeleton_is_created + run_generator + + default_files.each { |path| assert_file path } + skipped_files.each { |path| assert_no_file path } + end + + def test_api_modified_files + run_generator + + 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 'jbuilder'/, content) + assert_no_match(/gem 'web-console'/, content) + assert_match(/gem 'active_model_serializers'/, content) + end + + assert_file "config/application.rb" do |content| + assert_match(/config.api_only = true/, content) + end + + assert_file "config/initializers/cors.rb" + + assert_file "config/initializers/wrap_parameters.rb" + + assert_file "app/controllers/application_controller.rb", /ActionController::API/ + end + + private + + def default_files + files = %W( + .gitignore + Gemfile + Rakefile + config.ru + app/controllers + app/mailers + app/models + config/environments + config/initializers + config/locales + db + lib + lib/tasks + log + test/fixtures + test/controllers + test/integration + test/models + tmp + vendor + ) + files.concat %w(bin/bundle bin/rails bin/rake) + files + end + + def skipped_files + %w(app/assets + app/helpers + app/views + config/initializers/assets.rb + config/initializers/cookies_serializer.rb + config/initializers/session_store.rb + lib/assets + vendor/assets + test/helpers + tmp/cache/assets) + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 2bfa05a0b8..daf362357c 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -44,6 +44,7 @@ DEFAULT_APP_FILES = %w( vendor/assets vendor/assets/stylesheets vendor/assets/javascripts + tmp tmp/cache tmp/cache/assets ) @@ -498,7 +499,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_application_name_with_spaces - path = File.join(destination_root, "foo bar".shellescape) + path = File.join(destination_root, "foo bar") # This also applies to MySQL apps but not with SQLite run_generator [path, "-d", 'postgresql'] @@ -516,7 +517,7 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--dev"] assert_file "Gemfile" do |content| - assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) + assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) assert_no_match(/gem 'web-console', '~> 2.0'/, content) end end @@ -525,7 +526,7 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--edge"] assert_file "Gemfile" do |content| - assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) + assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) assert_no_match(/gem 'web-console', '~> 2.0'/, content) end end @@ -606,6 +607,32 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_create_keeps + run_generator + folders_with_keep = %w( + app/assets/images + app/mailers + app/models + app/controllers/concerns + app/models/concerns + lib/tasks + lib/assets + log + test/fixtures + test/fixtures/files + test/controllers + test/mailers + test/models + test/helpers + test/integration + tmp + vendor/assets/stylesheets + ) + folders_with_keep.each do |folder| + assert_file("#{folder}/.keep") + end + end + def test_psych_gem run_generator gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index d6e5f4bd89..afccf9d885 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -38,7 +38,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } - assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: [\"application\", \"destroy\", \"plugin\", \"runner\", \"test\"]\n", content + assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content @@ -313,6 +313,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/test_helper.rb" do |content| assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) + assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content) end end @@ -322,7 +323,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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.0.1"\n end\nend/ + 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"/ @@ -341,7 +342,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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.0.1"\n end\nend/ + 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"/ @@ -360,7 +361,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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.0.1"\n end\n end\nend/ + 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"/ @@ -544,6 +545,60 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_skipping_useless_folders_generation_for_api_engines + ['--full', '--mountable'].each do |option| + run_generator [destination_root, option, '--api'] + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + + FileUtils.rm_rf destination_root + end + end + + def test_application_controller_parent_for_mountable_api_plugins + run_generator [destination_root, '--mountable', '--api'] + + assert_file "app/controllers/bukkits/application_controller.rb" do |content| + assert_match "ApplicationController < ActionController::API", content + end + end + + def test_dummy_api_application_for_api_plugins + run_generator [destination_root, '--api'] + + assert_file "test/dummy/config/application.rb" do |content| + assert_match "config.api_only = true", content + end + end + + + def test_api_generators_configuration_for_api_engines + run_generator [destination_root, '--full', '--api'] + + assert_file "lib/bukkits/engine.rb" do |content| + assert_match "config.generators.api_only = true", content + end + end + + def test_scaffold_generator_for_mountable_api_plugins + run_generator [destination_root, '--mountable', '--api'] + + capture(:stdout) do + `#{destination_root}/bin/rails g scaffold article` + end + + assert_file "app/models/bukkits/article.rb" + assert_file "app/controllers/bukkits/articles_controller.rb" do |content| + assert_match "only: [:show, :update, :destroy]", content + end + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + end + protected def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 34e752cea1..95ef853a11 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -174,4 +174,73 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end end + + def test_controller_tests_pass_by_default_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 g controller dashboard foo` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_controller_tests_pass_by_default_inside_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails g controller dashboard foo` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_api_only_generates_a_proper_api_controller + run_generator ["User", "--api"] + + assert_file "app/controllers/users_controller.rb" do |content| + assert_match(/class UsersController < ApplicationController/, content) + assert_no_match(/respond_to/, content) + + assert_match(/before_action :set_user, only: \[:show, :update, :destroy\]/, content) + + assert_instance_method :index, content do |m| + assert_match(/@users = User\.all/, m) + assert_match(/render json: @users/, m) + end + + assert_instance_method :show, content do |m| + assert_match(/render json: @user/, m) + end + + assert_instance_method :create, content do |m| + assert_match(/@user = User\.new\(user_params\)/, m) + assert_match(/@user\.save/, m) + assert_match(/@user\.errors/, m) + end + + assert_instance_method :update, content do |m| + assert_match(/@user\.update\(user_params\)/, m) + assert_match(/@user\.errors/, m) + end + + assert_instance_method :destroy, content do |m| + assert_match(/@user\.destroy/, m) + end + end + end + + def test_api_controller_tests + run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}", "--api"] + + assert_file "test/controllers/users_controller_test.rb" do |content| + assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/test "should get index"/, content) + assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_no_match(/assert_redirected_to/, content) + end + end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index ee06802874..0c3808a9a0 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -87,6 +87,76 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/stylesheets/product_lines.css" end + def test_api_scaffold_on_invoke + run_generator %w(product_line title:string product:belongs_to user:references --api --no-template-engine --no-helper --no-assets) + + # Model + assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/product_lines.yml" + assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/ + assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/ + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/resources :product_lines$/, route) + end + + # Controller + assert_file "app/controllers/product_lines_controller.rb" do |content| + assert_match(/class ProductLinesController < ApplicationController/, content) + assert_no_match(/respond_to/, content) + + assert_match(/before_action :set_product_line, only: \[:show, :update, :destroy\]/, content) + + assert_instance_method :index, content do |m| + assert_match(/@product_lines = ProductLine\.all/, m) + assert_match(/render json: @product_lines/, m) + end + + assert_instance_method :show, content do |m| + assert_match(/render json: @product_line/, m) + end + + assert_instance_method :create, content do |m| + assert_match(/@product_line = ProductLine\.new\(product_line_params\)/, m) + assert_match(/@product_line\.save/, m) + assert_match(/@product_line\.errors/, m) + end + + assert_instance_method :update, content do |m| + assert_match(/@product_line\.update\(product_line_params\)/, m) + assert_match(/@product_line\.errors/, m) + end + + assert_instance_method :destroy, content do |m| + assert_match(/@product_line\.destroy/, m) + end + end + + assert_file "test/controllers/product_lines_controller_test.rb" do |test| + assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test) + assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_no_match(/assert_redirected_to/, test) + end + + # Views + assert_no_file "app/views/layouts/product_lines.html.erb" + + %w(index show new edit _form).each do |view| + assert_no_file "app/views/product_lines/#{view}.html.erb" + end + + # Helpers + assert_no_file "app/helpers/product_lines_helper.rb" + + # Assets + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_no_file "app/assets/javascripts/product_lines.js" + assert_no_file "app/assets/stylesheets/product_lines.css" + end + def test_functional_tests_without_attributes run_generator ["product_line"] @@ -282,6 +352,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/assets/stylesheets/posts.css" end + def test_scaffold_generator_no_scaffold_stylesheet_with_switch_no_scaffold_stylesheet + run_generator [ "posts", "--no-scaffold-stylesheet" ] + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" + end + + def test_scaffold_generator_no_scaffold_stylesheet_with_switch_scaffold_stylesheet_false + run_generator [ "posts", "--scaffold-stylesheet=false" ] + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" + end + def test_scaffold_generator_with_switch_resource_route_false run_generator [ "posts", "--resource-route=false" ] assert_file "config/routes.rb" do |route| @@ -393,4 +477,60 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content) end end + + def test_scaffold_tests_pass_by_default_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 do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/8 runs, 13 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` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 3a2195035f..77372fb514 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -56,7 +56,7 @@ 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 @@ -86,15 +86,6 @@ module SharedGeneratorTests end end - def test_template_is_executed_when_supplied - path = "https://gist.github.com/josevalim/103208/raw/" - template = %{ say "It works!" } - template.instance_eval "def read; self; end" # Make the string respond to read - - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } - end - def test_template_is_executed_when_supplied_an_https_path path = "https://gist.github.com/josevalim/103208/raw/" template = %{ say "It works!" } diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 12630e4d01..96b54c7264 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -62,6 +62,13 @@ class PathsTest < ActiveSupport::TestCase assert_equal ["/foo/bar/baz"], @root["app/models"].to_a end + test "absolute current path" do + @root.add "config" + @root.add "config/locales" + + assert_equal "/foo/bar/config/locales", @root["config/locales"].absolute_current + end + test "adding multiple physical paths as an array" do @root.add "app", with: ["/app", "/app2"] assert_equal ["/app", "/app2"], @root["app"].to_a @@ -215,6 +222,12 @@ class PathsTest < ActiveSupport::TestCase assert_equal "*.rb", @root["app"].glob end + test "it should be possible to get extensions by glob" do + @root["app"] = "/app" + @root["app"].glob = "*.{rb,yml}" + assert_equal ["rb", "yml"], @root["app"].extensions + end + test "it should be possible to override a path's default glob without assignment" do @root.add "app", with: "/app", glob: "*.rb" assert_equal "*.rb", @root["app"].glob diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 423ece277e..5f4171d44b 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -122,5 +122,13 @@ module RailtiesTests assert_no_file "app/helpers/foo_bar/topics_helper.rb" end end + + def test_assert_file_with_special_characters + path = "#{app_path}/tmp" + file_name = "#{path}/v0.1.4~alpha+nightly" + FileUtils.mkdir_p path + FileUtils.touch file_name + assert_file file_name + end end end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index 77883612f5..3066ba82d6 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -32,6 +32,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(passing_test) @reporter.record(skipped_test) @reporter.report + assert_no_match 'Failed tests:', @output.string assert_rerun_snippet_count 0 end @@ -43,6 +44,19 @@ class TestUnitReporterTest < ActiveSupport::TestCase assert_rerun_snippet_count 1 end + test "allows to customize the executable in the rerun snippet" do + original_executable = Rails::TestUnitReporter.executable + begin + Rails::TestUnitReporter.executable = "bin/test" + @reporter.record(failed_test) + @reporter.report + + assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:6$}, @output.string + ensure + Rails::TestUnitReporter.executable = original_executable + end + end + private def assert_rerun_snippet_count(snippet_count) assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size diff --git a/railties/test/test_unit/runner_test.rb b/railties/test/test_unit/runner_test.rb deleted file mode 100644 index 9ea8b2c114..0000000000 --- a/railties/test/test_unit/runner_test.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'abstract_unit' -require 'env_helpers' -require 'rails/test_unit/runner' - -class TestUnitTestRunnerTest < ActiveSupport::TestCase - include EnvHelpers - - setup do - @options = Rails::TestRunner::Options - end - - test "shows the filtered backtrace by default" do - options = @options.parse([]) - assert_not options[:backtrace] - end - - test "has --backtrace (-b) option to show the full backtrace" do - options = @options.parse(["-b"]) - assert options[:backtrace] - - options = @options.parse(["--backtrace"]) - assert options[:backtrace] - end - - test "show full backtrace using BACKTRACE environment variable" do - switch_env "BACKTRACE", "true" do - options = @options.parse([]) - assert options[:backtrace] - end - end - - test "tests run in the test environment by default" do - options = @options.parse([]) - assert_equal "test", options[:environment] - end - - test "can run in a specific environment" do - options = @options.parse(["-e development"]) - assert_equal "development", options[:environment] - end - - test "parse the filename and line" do - file = "test/test_unit/runner_test.rb" - absolute_file = File.expand_path __FILE__ - options = @options.parse(["#{file}:20"]) - assert_equal absolute_file, options[:filename] - assert_equal 20, options[:line] - - options = @options.parse(["#{file}:"]) - assert_equal [absolute_file], options[:patterns] - assert_nil options[:line] - - options = @options.parse([file]) - assert_equal [absolute_file], options[:patterns] - assert_nil options[:line] - end - - test "find_method on same file" do - options = @options.parse(["#{__FILE__}:#{__LINE__}"]) - runner = Rails::TestRunner.new(options) - assert_equal "test_find_method_on_same_file", runner.find_method - end - - test "find_method on a different file" do - options = @options.parse(["foobar.rb:#{__LINE__}"]) - runner = Rails::TestRunner.new(options) - assert_nil runner.find_method - end - - test "run all tests in a directory" do - options = @options.parse([__dir__]) - - assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] - assert_nil options[:filename] - assert_nil options[:line] - end - - test "run multiple folders" do - application_dir = File.expand_path("#{__dir__}/../application") - - options = @options.parse([__dir__, application_dir]) - - assert_equal ["#{__dir__}/**/*_test.rb", "#{application_dir}/**/*_test.rb"], options[:patterns] - assert_nil options[:filename] - assert_nil options[:line] - - runner = Rails::TestRunner.new(options) - assert runner.test_files.size > 0 - end - - test "run multiple files and run one file by line" do - line = __LINE__ - absolute_file = File.expand_path(__FILE__) - options = @options.parse([__dir__, "#{__FILE__}:#{line}"]) - - assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] - assert_equal absolute_file, options[:filename] - assert_equal line, options[:line] - - runner = Rails::TestRunner.new(options) - assert_equal [absolute_file], runner.test_files, 'Only returns the file that running by line' - end - - test "running multiple files passing line number" do - line = __LINE__ - options = @options.parse(["foobar.rb:8", "#{__FILE__}:#{line}"]) - - assert_equal File.expand_path(__FILE__), options[:filename], 'Returns the last file' - assert_equal line, options[:line] - end -end diff --git a/tools/profile b/tools/profile index eb7fc7792b..191e73b3dd 100755 --- a/tools/profile +++ b/tools/profile @@ -117,9 +117,9 @@ rescue LoadError def camelize(uppercase_first_letter = true) string = self if uppercase_first_letter - string = string.sub(/^[a-z\d]*/) { $&.capitalize } + string = string.sub(/^[a-z\d]*/) { |match| match.capitalize } else - string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase } + string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase } end string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::') end diff --git a/tools/test.rb b/tools/test.rb new file mode 100644 index 0000000000..70f295b554 --- /dev/null +++ b/tools/test.rb @@ -0,0 +1,12 @@ +$: << File.expand_path("test", COMPONENT_ROOT) +require File.expand_path("../../load_paths", __FILE__) +require "rails/test_unit/minitest_plugin" + +module Rails + # Necessary to get rerun-snippts working. + def self.root + @root ||= Pathname.new(COMPONENT_ROOT) + end +end + +Rails::TestUnitReporter.executable = "bin/test" |