diff options
1218 files changed, 25457 insertions, 11918 deletions
diff --git a/.gitignore b/.gitignore index bc96284375..c3cb009140 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ debug.log .Gemfile /.bundle /.ruby-version -/Gemfile.lock pkg /dist /doc/rdoc diff --git a/.travis.yml b/.travis.yml index ab7d968852..2237ca85d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ language: ruby sudo: false script: 'ci/travis.rb' before_install: - - "rvm current | grep 'jruby' && export AR_JDBC=true || echo" + - gem install bundler + - "rm ${BUNDLE_GEMFILE}.lock" before_script: - bundle update cache: bundler @@ -12,25 +13,24 @@ env: matrix: - "GEM=railties" - "GEM=ap" - - "GEM=aj" - - "GEM=am,amo,as,av" + - "GEM=am,amo,as,av,aj" - "GEM=ar:mysql" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - "GEM=aj:integration" + - "GEM=guides" rvm: - - 2.2 + - 2.2.2 - ruby-head - rbx-2 - - jruby + - jruby-head matrix: allow_failures: - env: "GEM=ar:mysql" + - rvm: ruby-head - rvm: rbx-2 - - rvm: jruby - - env: "GEM=aj" - - env: "GEM=aj:integration" + - rvm: jruby-head fast_finish: true notifications: email: false @@ -44,7 +44,7 @@ notifications: on_failure: always rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" -bundler_args: --without test +bundler_args: --without test --jobs 3 --retry 3 services: - memcached - redis diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ba2e53ef2..699b6fd2d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,9 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea * If you want to contribute to Rails documentation, please read the [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) section of the aforementioned guide. -*We only accept bug reports and pull requests in GitHub*. +*We only accept bug reports and pull requests on GitHub*. -* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). +* If you have a question about how to use Ruby on Rails, please [ask it on the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). * If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. @@ -15,11 +15,15 @@ gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master' gem 'coffee-rails', '~> 4.1.0' gem 'turbolinks' gem 'arel', github: 'rails/arel', branch: 'master' +gem 'mail', github: 'mikel/mail' + +gem 'sprockets', '~> 3.0.0.rc.1' +gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master' # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. -gem 'bcrypt', '~> 3.1.7', require: false +gem 'bcrypt', '~> 3.1.10', require: false # This needs to be with require false to avoid # it being automatically loaded by sprockets @@ -27,12 +31,12 @@ gem 'uglifier', '>= 1.3.0', require: false group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 3.2.2', platforms: :ruby + gem 'redcarpet', '~> 3.2.3', platforms: :ruby gem 'w3c_validators' - gem 'kindlerb' + gem 'kindlerb', '0.1.1' end -# AS +# ActiveSupport gem 'dalli', '>= 2.2.1' # ActiveJob @@ -42,13 +46,13 @@ group :job do gem 'sidekiq', require: false gem 'sucker_punch', require: false gem 'delayed_job', require: false - gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby - gem 'sneakers', '0.1.1.pre', require: false + gem 'queue_classic', require: false, platforms: :ruby + gem 'sneakers', require: false gem 'que', require: false gem 'backburner', require: false gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false gem 'qu-redis', require: false - # gem 'delayed_job_active_record', require: false + gem 'delayed_job_active_record', require: false gem 'sequel', require: false end @@ -60,18 +64,11 @@ group :test do # FIX: Our test suite isn't ready to run in random order yet gem 'minitest', '< 5.3.4' - platforms :mri_19 do - gem 'ruby-prof', '~> 0.11.2' - end - - platforms :mri_21 do + platforms :mri do gem 'stackprof' + gem 'byebug' end - # platforms :mri do - # gem 'byebug' - # end - gem 'benchmark-ips' end @@ -81,13 +78,13 @@ platforms :ruby do # Needed for compiling the ActionDispatch::Journey parser gem 'racc', '>=1.4.6', require: false - # AR + # ActiveRecord gem 'sqlite3', '~> 1.3.6' group :db do - gem 'pg', '>= 0.15.0' + gem 'pg', '>= 0.18.0' gem 'mysql', '>= 2.9.0' - gem 'mysql2', '>= 0.3.13' + gem 'mysql2', '>= 0.3.18' end end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..3cc20f1c9c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,311 @@ +GIT + remote: git://github.com/bkeepers/qu.git + revision: d098e2657c92e89a6413bebd9c033930759c061f + branch: master + specs: + qu (0.2.0) + qu-rails (0.2.0) + qu (= 0.2.0) + railties (>= 3.2, < 5) + qu-redis (0.2.0) + qu (= 0.2.0) + redis-namespace + +GIT + remote: git://github.com/mikel/mail.git + revision: b159e0a542962fdd5e292a48cfffa560d7cf412e + specs: + mail (2.6.3.edge) + mime-types (>= 1.16, < 3) + +GIT + remote: git://github.com/rails/arel.git + revision: aac9da257f291ad8d2d4f914528881c240848bb2 + branch: master + specs: + arel (7.0.0.alpha) + +GIT + remote: git://github.com/rails/jquery-rails.git + revision: 272abdd319bb3381b23182b928b25320590096b0 + branch: master + specs: + jquery-rails (4.0.3) + 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 + branch: master + specs: + sprockets-rails (3.0.0.beta1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0, < 4.0) + +PATH + remote: . + specs: + actionmailer (5.0.0.alpha) + actionpack (= 5.0.0.alpha) + actionview (= 5.0.0.alpha) + activejob (= 5.0.0.alpha) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (5.0.0.alpha) + actionview (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + rack (~> 1.6) + rack-test (~> 0.6.3) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.0.alpha) + activesupport (= 5.0.0.alpha) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (5.0.0.alpha) + activesupport (= 5.0.0.alpha) + globalid (>= 0.3.0) + activemodel (5.0.0.alpha) + activesupport (= 5.0.0.alpha) + builder (~> 3.1) + activerecord (5.0.0.alpha) + activemodel (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + arel (= 7.0.0.alpha) + activesupport (5.0.0.alpha) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + rails (5.0.0.alpha) + actionmailer (= 5.0.0.alpha) + actionpack (= 5.0.0.alpha) + actionview (= 5.0.0.alpha) + activejob (= 5.0.0.alpha) + activemodel (= 5.0.0.alpha) + activerecord (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.0.alpha) + sprockets-rails + railties (5.0.0.alpha) + actionpack (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + +GEM + remote: https://rubygems.org/ + specs: + amq-protocol (1.9.2) + backburner (0.4.6) + beaneater (~> 0.3.1) + 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) + builder (3.2.2) + bunny (1.7.0) + amq-protocol (>= 1.9.2) + byebug (4.0.5) + columnize (= 0.9.0) + celluloid (0.16.0) + 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-source + execjs + coffee-script-source (1.9.0) + columnize (0.9.0) + connection_pool (2.1.1) + dalli (2.7.2) + dante (0.1.5) + 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) + hitimes (1.2.2) + hitimes (1.2.2-x86-mingw32) + i18n (0.7.0) + json (1.8.2) + kindlerb (0.1.1) + mustache + nokogiri + loofah (2.0.1) + nokogiri (>= 1.5.9) + metaclass (0.0.4) + method_source (0.8.2) + mime-types (2.4.3) + 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) + mysql (2.9.1) + mysql2 (0.3.18) + 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) + psych (2.0.13) + que (0.9.2) + queue_classic (3.1.0) + pg (>= 0.17, < 0.19) + racc (1.4.12) + rack (1.6.0) + rack-cache (1.2) + rack (>= 0.4) + rack-protection (1.5.3) + rack + rack-test (0.6.3) + rack (>= 1.0) + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.6) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.2) + loofah (~> 2.0) + rake (10.4.2) + rdoc (4.2.0) + redcarpet (3.2.3) + redis (3.2.1) + redis-namespace (1.5.1) + redis (~> 3.0, >= 3.0.4) + resque (1.25.2) + mono_logger (~> 1.0) + multi_json (~> 1.0) + redis-namespace (~> 1.3) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) + resque-scheduler (4.0.0) + mono_logger (~> 1.0) + redis (~> 3.0) + resque (~> 1.25) + rufus-scheduler (~> 3.0) + rufus-scheduler (3.0.9) + tzinfo + sdoc (0.4.1) + json (~> 1.7, >= 1.7.7) + rdoc (~> 4.0) + sequel (4.19.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) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + sneakers (1.0.4) + bunny (~> 1.7.0) + serverengine (~> 1.5.5) + thor + thread (~> 0.1.7) + sprockets (3.0.2) + rack (~> 1.0) + sqlite3 (1.3.10) + stackprof (0.2.7) + sucker_punch (1.3.2) + celluloid (~> 0.16.0) + thor (0.19.1) + thread (0.1.7) + thread_safe (0.3.5) + tilt (1.4.1) + timers (4.0.1) + hitimes + turbolinks (2.5.3) + coffee-rails + tzinfo (1.2.2) + thread_safe (~> 0.1) + uglifier (2.7.0) + execjs (>= 0.3.0) + json (>= 1.8.0) + vegas (0.1.11) + rack (>= 1.0.0) + w3c_validators (1.2) + json + nokogiri + +PLATFORMS + ruby + x64-mingw32 + x86-mingw32 + +DEPENDENCIES + activerecord-jdbcmysql-adapter (>= 1.3.0) + activerecord-jdbcpostgresql-adapter (>= 1.3.0) + activerecord-jdbcsqlite3-adapter (>= 1.3.0) + arel! + backburner + bcrypt (~> 3.1.10) + benchmark-ips + byebug + coffee-rails (~> 4.1.0) + dalli (>= 2.2.1) + delayed_job + delayed_job_active_record + jquery-rails! + json + kindlerb (= 0.1.1) + mail! + minitest (< 5.3.4) + mocha (~> 0.14) + mysql (>= 2.9.0) + mysql2 (>= 0.3.18) + nokogiri (>= 1.4.5) + pg (>= 0.18.0) + psych (~> 2.0) + qu-rails! + qu-redis + que + queue_classic + racc (>= 1.4.6) + rack-cache (~> 1.2) + rails! + rake (>= 10.3) + redcarpet (~> 3.2.3) + resque + resque-scheduler + sdoc (~> 0.4.0) + sequel + sidekiq + sneakers + sprockets (~> 3.0.0.rc.1) + sprockets-rails! + sqlite3 (~> 1.3.6) + stackprof + sucker_punch + turbolinks + uglifier (>= 1.3.0) + w3c_validators + +BUNDLED WITH + 1.10.2 diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index ee5e6a91c1..a020b958e1 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -25,17 +25,6 @@ for Rails. You can check the status of his tests here: Do not release with Red AWDwR tests. -=== Are the supported plugins working? If not, make it work. - -Some Rails plugins are important and need to be supported until Rails 5. -As these plugins are outside the Rails repository it is easy to break them without knowing -after some refactoring or bug fix, so it is important to check if the following plugins -are working with the versions that will be released: - -* https://github.com/rails/protected_attributes - -Do not release red plugins tests. - === Do we have any Git dependencies? If so, contact those authors. Having Git dependencies indicates that we depend on unreleased code. @@ -107,7 +96,7 @@ Run `rake install` to generate the gems and install them locally. Then try 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 realise it is broken. +then realize it is broken. === Release the gem. diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 787dc46f00..7c1b0b215a 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,58 @@ +* 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. + + *Yuji Yaginuma* + +* Add support for inline images in mailer previews by using an interceptor + class to convert cid: urls in image src attributes to data urls. + + *Andrew White* + +* Mailer preview now uses `url_for` to fix links to emails for apps running on + a subdirectory. + + *Remo Mueller* + +* Mailer previews no longer crash when the `mail` method wasn't called + (`NullMail`). + + Fixes #19849. + + *Yves Senn* + +* Make sure labels and values line up in mailer previews. + + *Yves Senn* + +* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. + + Example: + + def test_emails + assert_enqueued_emails 2 do + ContactMailer.welcome.deliver_later + ContactMailer.welcome.deliver_later + end + end + + def test_no_emails + assert_no_enqueued_emails do + # No emails enqueued here + end + end + + *George Claghorn* + +* Add `_mailer` suffix to mailers created via generator, following the same + naming convention used in controllers and jobs. + + *Carlos Souza* + * Remove deprecate `*_path` helpers in email views. *Rafael Mendonça França* diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index a4e660d621..e5c2ed8c77 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -146,7 +146,7 @@ The Base class has the full list of configuration options. Here's an example: The latest version of Action Mailer can be installed with RubyGems: - % [sudo] gem install actionmailer + % gem install actionmailer Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index 8f4de8fafa..7197ea5e27 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' desc "Default Task" task default: [ :test ] @@ -20,16 +19,3 @@ namespace :test do end or raise "Failures" end end - -spec = eval(File.read('actionmailer.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task release: :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 513c217733..782b208ef4 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Email composition, delivery, and receiving framework (part of Rails).' s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' 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.rb b/actionmailer/lib/action_mailer.rb index 17d8dcc208..291a8c1e34 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -40,6 +40,7 @@ module ActionMailer autoload :Base autoload :DeliveryMethods + autoload :InlinePreviewInterceptor autoload :MailHelper autoload :Preview autoload :Previews, 'action_mailer/preview' diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 53cc1fdb31..7022c04a9a 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -21,11 +21,11 @@ module ActionMailer # the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments. # # class ApplicationMailer < ActionMailer::Base - # default from: 'from@exmaple.com' + # default from: 'from@example.com' # layout 'mailer' # end # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # default from: 'no-reply@example.com', # return_path: 'system@example.com' # @@ -58,7 +58,7 @@ module ActionMailer # # The mail method, if not passed a block, will inspect your views and send all the views with # the same name as the method, so the above action would send the +welcome.text.erb+ view - # file as well as the +welcome.text.html.erb+ view file in a +multipart/alternative+ email. + # file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email. # # If you want to explicitly render only certain templates, pass a block: # @@ -88,7 +88,7 @@ module ActionMailer # # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same # name as the method in your mailer model. For example, in the mailer defined above, the template at - # <tt>app/views/notifier/welcome.text.erb</tt> would be used to generate the email. + # <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email. # # Variables defined in the methods of your mailer model are accessible as instance variables in their # corresponding view. @@ -134,25 +134,28 @@ module ActionMailer # # = Sending mail # - # Once a mailer action and template are defined, you can deliver your message or create it and save it - # for delivery later: + # Once a mailer action and template are defined, you can deliver your message or defer its creation and + # delivery for later: # - # Notifier.welcome(User.first).deliver_now # sends the email - # mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object - # mail.deliver_now # sends the email + # NotifierMailer.welcome(User.first).deliver_now # sends the email + # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object + # mail.deliver_now # generates and sends the email now # - # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If - # you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on - # the <tt>ActionMailer::MessageDelivery</tt> object. + # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call + # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>, + # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object. # - # Notifier.welcome(User.first).message # => a Mail::Message object + # NotifierMailer.welcome(User.first).message # => a Mail::Message object # - # Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside - # of the request-response cycle, so the user doesn't have to wait on it): + # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background + # (example: outside of the request-response cycle, so the user doesn't have to wait on it): # - # Notifier.welcome(User.first).deliver_later # enqueue the email sending to Active Job + # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job + # + # 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 methods are expected to return a message object to be sent. # # = Multipart Emails # @@ -179,7 +182,7 @@ module ActionMailer # # Sending attachment in emails is easy: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # def welcome(recipient) # attachments['free_book.pdf'] = File.read('path/to/file.pdf') # mail(to: recipient, subject: "New account information") @@ -195,7 +198,7 @@ module ActionMailer # If you need to send attachments with no content, you need to create an empty view for it, # or add an empty body parameter like this: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # def welcome(recipient) # attachments['free_book.pdf'] = File.read('path/to/file.pdf') # mail(to: recipient, subject: "New account information", body: "") @@ -207,7 +210,7 @@ module ActionMailer # You can also specify that a file should be displayed inline with other HTML. This is useful # if you want to display a corporate logo or a photo. # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # def welcome(recipient) # attachments.inline['photo.png'] = File.read('path/to/photo.png') # mail(to: recipient, subject: "Here is what we look like") @@ -246,7 +249,7 @@ module ActionMailer # Action Mailer provides some intelligent defaults for your emails, these are usually specified in a # default method inside the class definition: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # default sender: 'system@example.com' # end # @@ -254,8 +257,8 @@ module ActionMailer # <tt>ActionMailer::Base</tt> sets the following: # # * <tt>mime_version: "1.0"</tt> - # * <tt>charset: "UTF-8",</tt> - # * <tt>content_type: "text/plain",</tt> + # * <tt>charset: "UTF-8"</tt> + # * <tt>content_type: "text/plain"</tt> # * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt> # # <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields, @@ -264,7 +267,7 @@ module ActionMailer # As you can pass in any header, you need to either quote the header as a string, or pass it in as # an underscored symbol, so the following will work: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # default 'Content-Transfer-Encoding' => '7bit', # content_description: 'This is a description' # end @@ -272,7 +275,7 @@ module ActionMailer # Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you # can define methods that evaluate as the message is being generated: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # default 'X-Special-Header' => Proc.new { my_method } # # private @@ -283,7 +286,7 @@ module ActionMailer # end # # Note that the proc is evaluated right at the start of the mail message generation, so if you - # set something in the defaults using a proc, and then set the same thing inside of your + # set something in the default using a proc, and then set the same thing inside of your # mailer method, it will get over written by the mailer method. # # It is also possible to set these default options that will be used in all mailers through @@ -297,7 +300,7 @@ module ActionMailer # This may be useful, for example, when you want to add default inline attachments for all # messages sent out by a certain mailer class: # - # class Notifier < ApplicationMailer + # class NotifierMailer < ApplicationMailer # before_action :add_inline_attachment! # # def welcome @@ -316,8 +319,9 @@ module ActionMailer # callbacks in the same manner that you would use callbacks in classes that # inherit from <tt>ActionController::Base</tt>. # - # Note that unless you have a specific reason to do so, you should prefer using before_action - # rather than after_action in your Action Mailer classes so that headers are parsed properly. + # Note that unless you have a specific reason to do so, you should prefer + # using <tt>before_action</tt> rather than <tt>after_action</tt> in your + # Action Mailer classes so that headers are parsed properly. # # = Previewing emails # @@ -325,9 +329,9 @@ module ActionMailer # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting # with database data, you'll need to write some scenarios to load messages with fake data: # - # class NotifierPreview < ActionMailer::Preview + # class NotifierMailerPreview < ActionMailer::Preview # def welcome - # Notifier.welcome(User.first) + # NotifierMailer.welcome(User.first) # end # end # @@ -376,8 +380,8 @@ module ActionMailer # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting. # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the # authentication type here. - # This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will - # send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange + # This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will + # send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange # information and a cryptographic Message Digest 5 algorithm to hash important information) # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts # to use it. Defaults to <tt>true</tt>. @@ -409,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 @@ -593,6 +599,7 @@ module ActionMailer class NullMail #:nodoc: def body; '' end + def header; {} end def respond_to?(string, include_all=false) true 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 new file mode 100644 index 0000000000..6d02b39225 --- /dev/null +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -0,0 +1,61 @@ +require 'base64' + +module ActionMailer + # Implements a mailer preview interceptor that converts image tag src attributes + # that use inline cid: style urls to data: style urls so that they are visible + # when previewing a HTML email in a web browser. + # + # 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) + # + class InlinePreviewInterceptor + PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i + + include Base64 + + def self.previewing_email(message) #:nodoc: + new(message).transform! + end + + def initialize(message) #:nodoc: + @message = message + end + + def transform! #:nodoc: + return message if html_part.blank? + + html_source.gsub!(PATTERN) do |match| + if part = find_part(match[9..-2]) + %[src="#{data_url(part)}"] + else + match + end + end + + message + end + + private + def message + @message + end + + def html_part + @html_part ||= message.html_part + end + + def html_source + html_part.body.raw_source + end + + def data_url(part) + "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" + end + + def find_part(cid) + message.all_parts.find{ |p| p.attachment? && p.cid == cid } + end + end +end 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/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index cc7935a7e0..239974e7b1 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -4,7 +4,17 @@ module ActionMailer # attachments list. module MailHelper # Take the text and format it, indented two spaces for each line, and - # wrapped at 72 columns. + # wrapped at 72 columns: + # + # text = <<-TEXT + # This is + # the paragraph. + # + # * item1 * item2 + # TEXT + # + # block_format text + # # => " This is the paragraph.\n\n * item1\n * item2\n" def block_format(text) formatted = text.split(/\n\r?\n/).collect { |paragraph| format_paragraph(paragraph) @@ -33,6 +43,8 @@ module ActionMailer end # Returns +text+ wrapped at +len+ columns and indented +indent+ spaces. + # By default column length +len+ equals 72 characters and indent + # +indent+ equal two spaces. # # my_text = 'Here is a sample text with more than 40 characters' # diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 44cf6665ba..25ad7ee721 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -21,7 +21,7 @@ module ActionMailer # :nodoc: mattr_accessor :preview_interceptors, instance_writer: false - self.preview_interceptors = [] + self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor] end module ClassMethods diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 6ddacf7b79..4d03a616d2 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,7 +1,11 @@ +require 'active_job' + module ActionMailer # Provides helper methods for testing Action Mailer, including #assert_emails # and #assert_no_emails module TestHelper + include ActiveJob::TestHelper + # Asserts that the number of emails sent matches the given number. # # def test_emails @@ -30,7 +34,7 @@ module ActionMailer original_count = ActionMailer::Base.deliveries.size yield new_count = ActionMailer::Base.deliveries.size - assert_equal original_count + number, new_count, "#{number} emails expected, but #{new_count - original_count} were sent" + assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" else assert_equal number, ActionMailer::Base.deliveries.size end @@ -58,5 +62,52 @@ module ActionMailer def assert_no_emails(&block) assert_emails 0, &block end + + # Asserts that the number of emails enqueued for later delivery matches + # the given number. + # + # def test_emails + # assert_enqueued_emails 0 + # ContactMailer.welcome.deliver_later + # assert_enqueued_emails 1 + # ContactMailer.welcome.deliver_later + # assert_enqueued_emails 2 + # end + # + # If a block is passed, that block should cause the specified number of + # emails to be enqueued. + # + # def test_emails_again + # assert_enqueued_emails 1 do + # ContactMailer.welcome.deliver_later + # end + # + # assert_enqueued_emails 2 do + # ContactMailer.welcome.deliver_later + # ContactMailer.welcome.deliver_later + # end + # end + def assert_enqueued_emails(number, &block) + assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block + end + + # Asserts that no emails are enqueued for later delivery. + # + # def test_no_emails + # assert_no_enqueued_emails + # ContactMailer.welcome.deliver_later + # assert_enqueued_emails 1 + # end + # + # If a block is provided, it should not cause any emails to be enqueued. + # + # def test_no_emails + # assert_no_enqueued_emails do + # # No emails should be enqueued from this block + # end + # end + def assert_no_enqueued_emails(&block) + assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block + end end end diff --git a/actionmailer/lib/rails/generators/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE index 323bb8a87f..2b0a078109 100644 --- a/actionmailer/lib/rails/generators/mailer/USAGE +++ b/actionmailer/lib/rails/generators/mailer/USAGE @@ -11,7 +11,7 @@ Example: rails generate mailer Notifications signup forgot_password invoice creates a Notifications mailer class, views, and test: - Mailer: app/mailers/notifications.rb - Views: app/views/notifications/signup.text.erb [...] - Test: test/mailers/notifications_test.rb + Mailer: app/mailers/notifications_mailer.rb + Views: app/views/notifications_mailer/signup.text.erb [...] + Test: test/mailers/notifications_mailer_test.rb diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 83f8a67da7..3ec7d3d896 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -4,16 +4,22 @@ module Rails source_root File.expand_path("../templates", __FILE__) argument :actions, type: :array, default: [], banner: "method method" - check_class_collision + + check_class_collision suffix: "Mailer" def create_mailer_file - template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}.rb") + template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}_mailer.rb") if self.behavior == :invoke template "application_mailer.rb", 'app/mailers/application_mailer.rb' end end hook_for :template_engine, :test_framework + + protected + def file_name + @_file_name ||= super.gsub(/\_mailer/i, '') + end end end end diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb index bce64a5e6e..348d314758 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb @@ -1,11 +1,11 @@ <% module_namespacing do -%> -class <%= class_name %> < ApplicationMailer +class <%= class_name %>Mailer < ApplicationMailer <% actions.each do |action| -%> # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # - # en.<%= file_path.tr("/",".") %>.<%= action %>.subject + # en.<%= file_path.tr("/",".") %>_mailer.<%= action %>.subject # def <%= action %> @greeting = "Hi" diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 7681679dc7..706249a2f6 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -11,11 +11,10 @@ end require 'active_support/testing/autorun' require 'action_mailer' require 'action_mailer/test_case' -require 'mail' # Emulate AV railtie require 'action_view' -ActionMailer::Base.send(:include, ActionView::Layouts) +ActionMailer::Base.include(ActionView::Layouts) # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true @@ -42,8 +41,3 @@ def jruby_skip(message = '') end require 'mocha/setup' # FIXME: stop using mocha - -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 5d9eda2555..59c5638f96 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'set' @@ -287,7 +286,7 @@ class BaseTest < ActiveSupport::TestCase end end - assert_nothing_raised { LateAttachmentAccessorMailer.welcome } + assert_nothing_raised { LateAttachmentAccessorMailer.welcome.message } end # Implicit multipart diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 2e4e019bf7..78507ce7dc 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'mail' class MyCustomDelivery end diff --git a/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb b/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb deleted file mode 100644 index a2187308b6..0000000000 --- a/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb +++ /dev/null @@ -1 +0,0 @@ -body_text
\ No newline at end of file diff --git a/actionmailer/test/fixtures/first_mailer/share.erb b/actionmailer/test/fixtures/first_mailer/share.erb deleted file mode 100644 index da43638ceb..0000000000 --- a/actionmailer/test/fixtures/first_mailer/share.erb +++ /dev/null @@ -1 +0,0 @@ -first mail diff --git a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb b/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb deleted file mode 100644 index 2d0cd5c124..0000000000 --- a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb +++ /dev/null @@ -1 +0,0 @@ -Have some dots. Enjoy!
\ No newline at end of file diff --git a/actionmailer/test/fixtures/second_mailer/share.erb b/actionmailer/test/fixtures/second_mailer/share.erb deleted file mode 100644 index 9a54010672..0000000000 --- a/actionmailer/test/fixtures/second_mailer/share.erb +++ /dev/null @@ -1 +0,0 @@ -second mail diff --git a/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb deleted file mode 100644 index 3b4ba35f20..0000000000 --- a/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb +++ /dev/null @@ -1 +0,0 @@ -let's go!
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml deleted file mode 100644 index 8dcf9746cc..0000000000 --- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%p Hello there, - -%p - Mr. - = @recipient - from haml
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml deleted file mode 100644 index 8dcf9746cc..0000000000 --- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml +++ /dev/null @@ -1,6 +0,0 @@ -%p Hello there, - -%p - Mr. - = @recipient - from haml
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb deleted file mode 100644 index 946d99ede5..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<html> - <body> - HTML formatted message to <strong><%= @recipient %></strong>. - </body> -</html> -<html> - <body> - HTML formatted message to <strong><%= @recipient %></strong>. - </body> -</html> diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~ b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~ deleted file mode 100644 index 946d99ede5..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~ +++ /dev/null @@ -1,10 +0,0 @@ -<html> - <body> - HTML formatted message to <strong><%= @recipient %></strong>. - </body> -</html> -<html> - <body> - HTML formatted message to <strong><%= @recipient %></strong>. - </body> -</html> diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb deleted file mode 100644 index 6940419d47..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb +++ /dev/null @@ -1 +0,0 @@ -Ignored when searching for implicitly multipart parts. diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak deleted file mode 100644 index 6940419d47..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak +++ /dev/null @@ -1 +0,0 @@ -Ignored when searching for implicitly multipart parts. diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb deleted file mode 100644 index a6c8d54cf9..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb +++ /dev/null @@ -1,2 +0,0 @@ -Plain text to <%= @recipient %>. -Plain text to <%= @recipient %>. diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb deleted file mode 100644 index c14348c770..0000000000 --- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb +++ /dev/null @@ -1 +0,0 @@ -yaml to: <%= @recipient %>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb deleted file mode 100644 index ae3cfa77e7..0000000000 --- a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb +++ /dev/null @@ -1 +0,0 @@ -Hey Ho, <%= render partial: "subtemplate" %>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb b/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb deleted file mode 100644 index 73ea14f82f..0000000000 --- a/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb +++ /dev/null @@ -1 +0,0 @@ -<strong>foo</strong> <%= @foo %>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb b/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb deleted file mode 100644 index 779fe4c1ea..0000000000 --- a/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb +++ /dev/null @@ -1 +0,0 @@ -foo: <%= @foo %>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/rxml_template.rxml b/actionmailer/test/fixtures/test_mailer/rxml_template.rxml deleted file mode 100644 index d566bd8d7c..0000000000 --- a/actionmailer/test/fixtures/test_mailer/rxml_template.rxml +++ /dev/null @@ -1,2 +0,0 @@ -xml.instruct! -xml.test
\ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb deleted file mode 100644 index 7afe1f651c..0000000000 --- a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -Hello there, - -Mr. <%= @recipient %>
\ No newline at end of file diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index 24ccaab8df..ff6b25b0c7 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -59,6 +59,12 @@ The second end end + def use_cache + mail_with_defaults do |format| + format.html { render(inline: "<% cache(:foo) do %>Greetings from a cache helper block<% end %>") } + end + end + protected def mail_with_defaults(&block) @@ -107,5 +113,11 @@ class MailerHelperTest < ActionMailer::TestCase TEXT assert_equal expected.gsub("\n", "\r\n"), mail.body.encoded end -end + def test_use_cache + assert_nothing_raised do + mail = HelperMailer.use_cache + assert_equal "Greetings from a cache helper block", mail.body.encoded + end + end +end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index 55ee00602a..862ce26187 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -1,9 +1,7 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_job' require 'minitest/mock' require 'mailers/delayed_mailer' -require 'active_support/core_ext/numeric/time' class MessageDeliveryTest < ActiveSupport::TestCase include ActiveJob::TestHelper @@ -11,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) @@ -22,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 @@ -82,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/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 96b75ff2e0..0a4bc75d3e 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -1,5 +1,5 @@ -# encoding: utf-8 require 'abstract_unit' +require 'active_support/testing/stream' class TestHelperMailer < ActionMailer::Base def test @@ -11,6 +11,8 @@ class TestHelperMailer < ActionMailer::Base end class TestHelperMailerTest < ActionMailer::TestCase + include ActiveSupport::Testing::Stream + def test_setup_sets_right_action_mailer_options assert_equal :test, ActionMailer::Base.delivery_method assert ActionMailer::Base.perform_deliveries @@ -110,6 +112,17 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_match(/1 .* but 2/, error.message) end + def test_assert_emails_message + TestHelperMailer.test.deliver_now + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_emails 2 do + TestHelperMailer.test.deliver_now + end + end + assert_match "Expected: 2", error.message + assert_match "Actual: 1", error.message + end + def test_assert_no_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_emails do @@ -119,6 +132,61 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_match(/0 .* but 1/, error.message) end + + def test_assert_enqueued_emails + assert_nothing_raised do + assert_enqueued_emails 1 do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_emails_too_few_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_emails 2 do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + + assert_match(/2 .* but 1/, error.message) + end + + def test_assert_enqueued_emails_too_many_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_emails 1 do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + TestHelperMailer.test.deliver_later + end + end + end + + assert_match(/1 .* but 2/, error.message) + end + + def test_assert_no_enqueued_emails + assert_nothing_raised do + assert_no_enqueued_emails do + TestHelperMailer.test.deliver_now + end + end + end + + def test_assert_no_enqueued_emails_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_enqueued_emails do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + + assert_match(/0 .* but 1/, error.message) + end end class AnotherTestHelperMailerTest < ActionMailer::TestCase diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 6c4ce6195e..7b2f150c67 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,278 @@ +* 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`). + + See #20017. + + *Eliot Sykes* + +* Deprecate `:nothing` option for `render` method. + + *Mehmet Emin İNAÇ* + +* Fix `rake routes` not showing the right format when + nesting multiple routes. + + See #18373. + + *Ravil Bayramgalin* + +* Add ability to override default form builder for a controller. + + class AdminController < ApplicationController + default_form_builder AdminFormBuilder + end + + *Kevin McPhillips* + +* For actions with no corresponding templates, render `head :no_content` + instead of raising an error. This allows for slimmer API controller + methods that simply work, without needing further instructions. + + See #19036. + + *Stephen Bussey* + +* Provide friendlier access to request variants. + + request.variant = :phone + request.variant.phone? # true + request.variant.tablet? # false + + request.variant = [:phone, :tablet] + request.variant.phone? # true + request.variant.desktop? # false + request.variant.any?(:phone, :desktop) # true + request.variant.any?(:desktop, :watch) # false + + *George Claghorn* + +* Fix regression where a gzip file response would have a Content-type, + even when it was a 304 status code. + + See #19271. + + *Kohei Suzuki* + +* Fix handling of empty `X_FORWARDED_HOST` header in `raw_host_with_port`. + + Previously, an empty `X_FORWARDED_HOST` header would cause + `Actiondispatch::Http:URL.raw_host_with_port` to return `nil`, causing + `Actiondispatch::Http:URL.host` to raise a `NoMethodError`. + + *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 + the `request_class` method. + + *tenderlove@ruby-lang.org* + +* Fallback to `ENV['RAILS_RELATIVE_URL_ROOT']` in `url_for`. + + Fixed an issue where the `RAILS_RELATIVE_URL_ROOT` environment variable is not + prepended to the path when `url_for` is called. If `SCRIPT_NAME` (used by Rack) + is set, it takes precedence. + + Fixes #5122. + + *Yasyf Mohamedali* + +* Partitioning of routes is now done when the routes are being drawn. This + helps to decrease the time spent filtering the routes during the first request. + + *Guo Xiang Tan* + +* Fix regression in functional tests. Responses should have default headers + assigned. + + See #18423. + + *Jeremy Kemper*, *Yves Senn* + +* Deprecate AbstractController#skip_action_callback in favor of individual skip_callback methods + (which can be made to raise an error if no callback was removed). + + *Iain Beeston* + +* Alias the `ActionDispatch::Request#uuid` method to `ActionDispatch::Request#request_id`. + Due to implementation, `config.log_tags = [:request_id]` also works in substitute + for `config.log_tags = [:uuid]`. + + *David Ilizarov* + +* Change filter on /rails/info/routes to use an actual path regexp from rails + and not approximate javascript version. Oniguruma supports much more + extensive list of features than javascript regexp engine. + + Fixes #18402. + + *Ravil Bayramgalin* + +* Non-string authenticity tokens do not raise NoMethodError when decoding + the masked token. + + *Ville Lautanala* + +* Add `http_cache_forever` to Action Controller, so we can cache a response + that never gets expired. + + *arthurnn* + +* `ActionController#translate` supports symbols as shortcuts. + When shortcut is given it also lookups without action name. + + *Max Melentiev* + +* Expand `ActionController::ConditionalGet#fresh_when` and `stale?` to also + accept a collection of records as the first argument, so that the + following code can be written in a shorter form. + + # Before + def index + @articles = Article.all + fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at)) + end + + # After + def index + @articles = Article.all + fresh_when(@articles) + end + + *claudiob* + +* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback + + Fixes an issue where a mounted rack app at root would intercept the HEAD + request causing an incorrect behavior during the fall back to GET requests. + + Example: + + draw do + get '/home' => 'test#index' + mount rack_app, at: '/' + end + head '/home' + assert_response :success + + In this case, a HEAD request runs through the routes the first time and fails + to match anything. Then, it runs through the list with the fallback and matches + `get '/home'`. The original behavior would match the rack app in the first pass. + + *Terence Sun* + +* Migrating xhr methods to keyword arguments syntax + in `ActionController::TestCase` and `ActionDispatch::Integration` + + Old syntax: + + xhr :get, :create, params: { id: 1 } + + New syntax example: + + get :create, params: { id: 1 }, xhr: true + + *Kir Shatrov* + +* Migrating to keyword arguments syntax in `ActionController::TestCase` and + `ActionDispatch::Integration` HTTP request methods. + + Example: + + post :create, params: { y: x }, session: { a: 'b' } + get :view, params: { id: 1 } + get :view, params: { id: 1 }, format: :json + + *Kir Shatrov* + +* Preserve default url options when generating URLs. + + 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. + + Use `follow_redirect!` manually after the request call for the same behavior. + + *Aditya Kapoor* + +* Add `ActionController::Renderer` to render arbitrary templates + outside controller actions. + + Its functionality is accessible through class methods `render` and + `renderer` of `ActionController::Base`. + + *Ravil Bayramgalin* + +* Support `:assigns` option when rendering with controllers/mailers. + + *Ravil Bayramgalin* + +* Default headers, removed in controller actions, are no longer reapplied on + the test response. + + *Jonas Baumann* + +* 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 + verification callback appended instead of prepended to the chain. + This allows you to let the verification step depend on prior callbacks. + + Example: + + class ApplicationController < ActionController::Base + before_action :authenticate + protect_from_forgery prepend: false, unless: -> { @authenticated_by.oauth? } + + private + def authenticate + if oauth_request? + # authenticate with oauth + @authenticated_by = 'oauth'.inquiry + else + # authenticate with cookies + @authenticated_by = 'cookie'.inquiry + end + end + end + + *Josef Šimánek* + +* Remove `ActionController::HideActions`. + + *Ravil Bayramgalin* + * Remove `respond_to`/`respond_with` placeholder methods, this functionality has been extracted to the `responders` gem. @@ -41,7 +316,7 @@ * Ensure `append_info_to_payload` is called even if an exception is raised. - Fixes an issue where when an exception is raised in the request the additonal + Fixes an issue where when an exception is raised in the request the additional payload data is not available. See: @@ -70,9 +345,9 @@ *Travis Grathwell* -* Stop converting empty arrays in `params` to `nil` +* Stop converting empty arrays in `params` to `nil`. - This behaviour was introduced in response to CVE-2012-2660, CVE-2012-2694 + This behavior was introduced in response to CVE-2012-2660, CVE-2012-2694 and CVE-2013-0155 ActiveRecord now issues a safe query when passing an empty array into diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 02a24a7412..44c980b070 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -28,7 +28,7 @@ can be used outside of Rails. The latest version of Action Pack can be installed with RubyGems: - % [sudo] gem install actionpack + % gem install actionpack Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 4b60b77759..601263bfac 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' test_files = Dir.glob('test/**/*_test.rb') @@ -9,10 +8,7 @@ task :default => :test # Run the unit tests Rake::TestTask.new do |t| t.libs << 'test' - - # make sure we include the tests in alphabetical order as on some systems - # this will not happen automatically and the tests (as a whole) will error - t.test_files = test_files.sort + t.test_files = test_files t.warning = true t.verbose = true @@ -27,19 +23,6 @@ namespace :test do end end -spec = eval(File.read('actionpack.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - task :lines do load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' files = FileList["lib/**/*.rb"] diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index f83823dd75..1bba9df969 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).' s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -21,9 +21,9 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.6.0' - s.add_dependency 'rack-test', '~> 0.6.2' - s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rack', '~> 1.6' + s.add_dependency 'rack-test', '~> 0.6.3' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_dependency 'actionview', version 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 8c7cec3561..c95b9a4097 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -57,21 +57,11 @@ module AbstractController controller.public_instance_methods(true) end - # The list of hidden actions. Defaults to an empty array. - # This can be modified by other modules or subclasses - # to specify particular actions as hidden. - # - # ==== Returns - # * <tt>Array</tt> - An array of method names that should not be considered actions. - def hidden_actions - [] - end - # A list of method names that should be considered actions. This # includes all public instance methods on a controller, less # any internal methods (see internal_methods), adding back in # any methods that are internal, but still exist on the class - # itself. Finally, hidden_actions are removed. + # itself. # # ==== Returns # * <tt>Set</tt> - A set of all methods that should be considered actions. @@ -82,9 +72,7 @@ module AbstractController # Except for public instance methods of Base and its ancestors internal_methods + # Be sure to include shadowed public instance methods of this class - public_instance_methods(false)).uniq.map(&:to_s) - - # And always exclude explicitly hidden actions - hidden_actions.to_a + public_instance_methods(false)).uniq.map(&:to_s) methods.to_set end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 32de82780f..13795f0dd8 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -28,6 +28,16 @@ module AbstractController # The basic idea is that <tt>:only => :index</tt> gets converted to # <tt>:if => proc {|c| c.action_name == "index" }</tt>. # + # Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they + # are used together. + # + # only: :index, if: -> { true } # the :if option will be ignored. + # + # Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they + # are used together. + # + # except: :index, if: -> { true } # the :except option will be ignored. + # # ==== Options # * <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 @@ -51,11 +61,16 @@ module AbstractController # impossible to skip a callback defined using an anonymous proc # using #skip_action_callback def skip_action_callback(*names) - skip_before_action(*names) - skip_after_action(*names) - skip_around_action(*names) + ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.') + skip_before_action(*names, raise: false) + skip_after_action(*names, raise: false) + skip_around_action(*names, raise: false) + end + + def skip_filter(*names) + ActiveSupport::Deprecation.warn("`skip_filter` is deprecated and will be removed in Rails 5.1. Use skip_before_action, skip_after_action or skip_around_action instead.") + skip_action_callback(*names) end - alias_method :skip_filter, :skip_action_callback # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract @@ -170,14 +185,22 @@ module AbstractController set_callback(:process_action, callback, name, options) end end - alias_method :"#{callback}_filter", :"#{callback}_action" + + define_method "#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.") + send("#{callback}_action", *names, &blk) + end define_method "prepend_#{callback}_action" do |*names, &blk| _insert_callbacks(names, blk) do |name, options| set_callback(:process_action, callback, name, options.merge(:prepend => true)) end end - alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" + + define_method "prepend_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use prepend_#{callback}_action instead.") + send("prepend_#{callback}_action", *names, &blk) + end # Skip a before, after or around callback. See _insert_callbacks # for details on the allowed parameters. @@ -186,11 +209,19 @@ module AbstractController skip_callback(:process_action, callback, name, options) end end - alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" + + define_method "skip_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use skip_#{callback}_action instead.") + send("skip_#{callback}_action", *names, &blk) + end # *_action is the same as append_*_action alias_method :"append_#{callback}_action", :"#{callback}_action" - alias_method :"append_#{callback}_filter", :"#{callback}_action" + + define_method "append_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use append_#{callback}_action instead.") + send("append_#{callback}_action", *names, &blk) + end end end end diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index 568c47e43a..14b574e322 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -6,9 +6,9 @@ module AbstractController define_method(:inherited) do |klass| super(klass) if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } - klass.send(:include, namespace.railtie_routes_url_helpers(include_path_helpers)) + klass.include(namespace.railtie_routes_url_helpers(include_path_helpers)) else - klass.send(:include, routes.url_helpers(include_path_helpers)) + klass.include(routes.url_helpers(include_path_helpers)) end end end diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 02028d8e05..56b8ce895e 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -8,14 +8,15 @@ module AbstractController # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive # to translate many keys within the same controller / action and gives you a # simple framework for scoping them consistently. - def translate(*args) - key = args.first - if key.is_a?(String) && (key[0] == '.') - key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }" - args[0] = key + def translate(key, options = {}) + if key.to_s.first == '.' + path = controller_path.tr('/', '.') + defaults = [:"#{path}#{key}"] + defaults << options[:default] if options[:default] + options[:default] = defaults + key = "#{path}.#{action_name}#{key}" end - - I18n.translate(*args) + I18n.translate(key, options) end alias :t :translate diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 91ac7eef01..89fc4520d3 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -7,13 +7,15 @@ require 'action_controller/metal/strong_parameters' module ActionController extend ActiveSupport::Autoload + autoload :API autoload :Base autoload :Caching autoload :Metal autoload :Middleware + autoload :Renderer + autoload :FormBuilder autoload_under "metal" do - autoload :Compatibility autoload :ConditionalGet autoload :Cookies autoload :DataStreaming @@ -22,8 +24,8 @@ module ActionController autoload :ForceSSL autoload :Head autoload :Helpers - autoload :HideActions 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..d8149e0232 --- /dev/null +++ b/actionpack/lib/action_controller/api.rb @@ -0,0 +1,145 @@ +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 functionality 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> and <tt>respond_with</tt> methods. + # Adding it is quite simple, you just need to include the module in a specific + # controller or in <tt>ApplicationController</tt> in case you want it + # available to your entire app: + # + # 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 in the argument: + # + # class MetalController + # ActionController::API.without_modules(:Redirecting, :DataStreaming).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 the earliest 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 5cb11bc479..2c3b3f4e05 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: # @@ -206,7 +206,6 @@ module ActionController AbstractController::AssetPaths, Helpers, - HideActions, UrlFor, Redirecting, ActionView::Layouts, @@ -222,6 +221,7 @@ module ActionController Cookies, Flash, + FormBuilder, RequestForgeryProtection, ForceSSL, Streaming, diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb new file mode 100644 index 0000000000..f2656ca894 --- /dev/null +++ b/actionpack/lib/action_controller/form_builder.rb @@ -0,0 +1,48 @@ +module ActionController + # Override the default form builder for all views rendered by this + # controller and any of its descendants. Accepts a subclass of + # +ActionView::Helpers::FormBuilder+. + # + # For example, given a form builder: + # + # class AdminFormBuilder < ActionView::Helpers::FormBuilder + # def special_field(name) + # end + # end + # + # The controller specifies a form builder as its default: + # + # class AdminAreaController < ApplicationController + # default_form_builder AdminFormBuilder + # end + # + # Then in the view any form using +form_for+ will be an instance of the + # specified form builder: + # + # <%= form_for(@instance) do |builder| %> + # <%= builder.special_field(:name) %> + # <% end %> + module FormBuilder + extend ActiveSupport::Concern + + included do + class_attribute :_default_form_builder, instance_accessor: false + end + + module ClassMethods + # Set the form builder to be used as the default for all forms + # in the views rendered by this controller and its subclasses. + # + # ==== Parameters + # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+ + def default_form_builder(builder) + self._default_form_builder = builder + end + end + + # Default form builder for the controller + def default_form_builder + self.class._default_form_builder + end + end +end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 993f8e150d..ae111e4951 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -190,11 +190,15 @@ module ActionController end def dispatch(name, request) #:nodoc: + set_request!(request) + process(name) + to_a + end + + def set_request!(request) #:nodoc: @_request = request @_env = request.env @_env['action_controller.instance'] = self - process(name) - to_a end def to_a #:nodoc: 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/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index b210ee3423..bb3ad9b850 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -15,7 +15,7 @@ module ActionController module ClassMethods # Allows you to consider additional controller-wide information when generating an ETag. # For example, if you serve pages tailored depending on who's logged in at the moment, you - # may want to add the current user id to be part of the ETag to prevent authorized displaying + # may want to add the current user id to be part of the ETag to prevent unauthorized displaying # of cached pages. # # class InvoicesController < ApplicationController @@ -40,7 +40,7 @@ module ActionController # * <tt>:etag</tt>. # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current # controller/action is included in ETags. If the action renders a # different template, you can include its digest instead. If the action @@ -51,21 +51,31 @@ module ActionController # # def show # @article = Article.find(params[:id]) - # fresh_when(etag: @article, last_modified: @article.created_at, public: true) + # fresh_when(etag: @article, last_modified: @article.updated_at, public: true) # end # # This will render the show template if the request isn't sending a matching ETag or # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # - # You can also just pass a record where +last_modified+ will be set by calling - # +updated_at+ and the +etag+ by passing the object itself. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) # fresh_when(@article) # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # fresh_when(@articles) + # end + # + # When passing a record or a collection, you can still set the public header: # # def show # @article = Article.find(params[:id]) @@ -77,18 +87,16 @@ module ActionController # # before_action { fresh_when @article, template: 'widgets/show' } # - def fresh_when(record_or_options, additional_options = {}) - if record_or_options.is_a? Hash - options = record_or_options - options.assert_valid_keys(:etag, :last_modified, :public, :template) - else - record = record_or_options - options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options) + def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil) + last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at) + + if etag || template + response.etag = combine_etags(etag: etag, last_modified: last_modified, + public: public, template: template) end - response.etag = combine_etags(options) if options[:etag] || options[:template] - response.last_modified = options[:last_modified] if options[:last_modified] - response.cache_control[:public] = true if options[:public] + response.last_modified = last_modified if last_modified + response.cache_control[:public] = true if public head :not_modified if request.fresh?(response) end @@ -103,7 +111,7 @@ module ActionController # * <tt>:etag</tt>. # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current # controller/action is included in ETags. If the action renders a # different template, you can include its digest instead. If the action @@ -115,7 +123,7 @@ module ActionController # def show # @article = Article.find(params[:id]) # - # if stale?(etag: @article, last_modified: @article.created_at) + # if stale?(etag: @article, last_modified: @article.updated_at) # @statistics = @article.really_expensive_call # respond_to do |format| # # all the supported formats @@ -123,8 +131,8 @@ module ActionController # end # end # - # You can also just pass a record where +last_modified+ will be set by calling - # +updated_at+ and the +etag+ by passing the object itself. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) @@ -137,7 +145,23 @@ module ActionController # end # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # + # if stale?(@articles) + # @statistics = @articles.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record or a collection, you can still set the public header: # # def show # @article = Article.find(params[:id]) @@ -157,8 +181,8 @@ module ActionController # super if stale? @article, template: 'widgets/show' # end # - def stale?(record_or_options, additional_options = {}) - fresh_when(record_or_options, additional_options) + def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil) + fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template) !request.fresh?(response) end @@ -191,6 +215,24 @@ module ActionController response.cache_control.replace(:no_cache => true) end + # Cache or yield the block. The cache is supposed to never expire. + # + # You can use this method when you have a HTTP response that never changes, + # and the browser and proxies should cache it indefinitely. + # + # * +public+: By default, HTTP responses are private, cached only on the + # user's web browser. To allow proxies to cache the response, set +true+ to + # indicate that they can serve the cached response to all users. + # + # * +version+: the version passed as a key for the cache. + def http_cache_forever(public: false, version: 'v1') + expires_in 100.years, public: public + + yield if stale?(etag: "#{version}-#{request.fullpath}", + last_modified: Time.parse('2011-01-01').utc, + public: public) + end + private def combine_etags(options) etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 0d93e2f7aa..70f42bf565 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -38,6 +38,8 @@ module ActionController headers.delete('Content-Type') headers.delete('Content-Length') end + + true end private diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index a9c3e438fb..b4da381d26 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -44,7 +44,7 @@ module ActionController # the output might look like this: # # 23 Aug 11:30 | Carolina Railhawks Soccer Match - # N/A | Carolina Railhaws Training Workshop + # N/A | Carolina Railhawks Training Workshop # module Helpers extend ActiveSupport::Concern @@ -93,6 +93,10 @@ module ActionController super(args) end + # Returns a list of helper names in a given path. + # + # ActionController::Base.all_helpers_from_path 'app/helpers' + # # => ["application", "chart", "rubygems"] def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb deleted file mode 100644 index af36ffa240..0000000000 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ /dev/null @@ -1,40 +0,0 @@ - -module ActionController - # Adds the ability to prevent public methods on a controller to be called as actions. - module HideActions - extend ActiveSupport::Concern - - included do - class_attribute :hidden_actions - self.hidden_actions = Set.new.freeze - end - - private - - # Overrides AbstractController::Base#action_method? to return false if the - # action name is in the list of hidden actions. - def method_for_action(action_name) - self.class.visible_action?(action_name) && super - end - - module ClassMethods - # Sets all of the actions passed in as hidden actions. - # - # ==== Parameters - # * <tt>args</tt> - A list of actions - def hide_action(*args) - self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze - end - - def visible_action?(action_name) - not hidden_actions.include?(action_name) - end - - # Overrides AbstractController::Base#action_methods to remove any methods - # that are listed as hidden methods. - def action_methods - @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze - end - end - end -end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index a219d35b25..fb0a52b076 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -74,16 +74,16 @@ module ActionController end end - def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) - authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end - def request_http_basic_authentication(realm = "Application") - HttpAuthentication::Basic.authentication_request(self, realm) + def request_http_basic_authentication(realm = "Application", message = nil) + HttpAuthentication::Basic.authentication_request(self, realm, message) end end @@ -106,21 +106,22 @@ module ActionController end def auth_scheme(request) - request.authorization.split(' ', 2).first + request.authorization.to_s.split(' ', 2).first end def auth_param(request) - request.authorization.split(' ', 2).second + request.authorization.to_s.split(' ', 2).second end def encode_credentials(user_name, password) "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + def authentication_request(controller, realm, message) + message ||= "HTTP Basic: Access denied.\n" + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") controller.status = 401 - controller.response_body = "HTTP Basic: Access denied.\n" + controller.response_body = message end end @@ -170,8 +171,8 @@ module ActionController extend self module ControllerMethods - def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) - authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message) end # Authenticate with HTTP Digest, returns true or false @@ -314,7 +315,7 @@ module ActionController nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end - # Opaque based on random generation - but changing each request? + # Opaque based on digest of secret key def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end @@ -396,21 +397,21 @@ 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 module ControllerMethods - def authenticate_or_request_with_http_token(realm = "Application", &login_procedure) - authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm) + def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message) end def authenticate_with_http_token(&login_procedure) Token.authenticate(self, &login_procedure) end - def request_http_token_authentication(realm = "Application") - Token.authentication_request(self, realm) + def request_http_token_authentication(realm = "Application", message = nil) + Token.authentication_request(self, realm, message) end end @@ -498,9 +499,10 @@ module ActionController # realm - String realm to use in the header. # # Returns nothing. - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") - controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized + 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 end end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ae04b53825..d66b2214ce 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,13 +1,15 @@ module ActionController module ImplicitRender - def send_action(method, *args) - ret = super - default_render unless performed? - ret - end + + include BasicImplicitRender def default_render(*args) - 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 + super + end end def method_for_action(action_name) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 7590fb6843..58150cd9a9 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,7 +102,7 @@ module ActionController end end - message = json.gsub(/\n/, "\ndata: ") + message = json.gsub("\n".freeze, "\ndata: ".freeze) @stream.write "data: #{message}\n\n" end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 7dae171215..fab1be3459 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -288,16 +288,17 @@ module ActionController #:nodoc: end def variant - if @variant.nil? + if @variant.empty? @variants[:none] || @variants[:any] - elsif (@variants.keys & @variant).any? - @variant.each do |v| - return @variants[v] if @variants.key?(v) - end else - @variants[:any] + @variants[variant_key] end end + + private + def variant_key + @variant.find { |variant| @variants.key?(variant) } || :any + end end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a7e734db42..8a4ea70649 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -8,8 +8,7 @@ module ActionController # POST requests without having to specify any root elements. # # This functionality is enabled in +config/initializers/wrap_parameters.rb+ - # and can be customized. If you are upgrading to \Rails 3.1, this file will - # need to be created for the functionality to be enabled. + # and can be customized. # # You could also turn it on per controller by setting the format array to # a non-empty array: @@ -131,7 +130,7 @@ module ActionController private # Determine the wrapper model from the controller's name. By convention, # this could be done by trying to find the defined model that has the - # same singularize name as the controller. For example, +UsersController+ + # same singular name as the controller. For example, +UsersController+ # will try to find if the +User+ model exists. # # This method also does namespace lookup. Foo::Bar::UsersController will @@ -251,7 +250,7 @@ module ActionController private - # Returns the wrapper key which will be used to stored wrapped parameters. + # Returns the wrapper key which will be used to store wrapped parameters. def _wrapper_key _wrapper_options.name end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index 545d4a7e6e..ae9d89cc8c 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -8,9 +8,15 @@ module ActionController delegate :headers, :status=, :location=, :content_type=, :status, :location, :content_type, :response_code, :to => "@_response" - def dispatch(action, request) + module ClassMethods + def build_with_env(env = {}) #:nodoc: + new.tap { |c| c.set_request! ActionDispatch::Request.new(env) } + end + end + + def set_request!(request) #:nodoc: + super set_response!(request) - super(action, request) end def response_body=(body) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 7bbff0450a..b9ae8dd5ea 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -4,6 +4,17 @@ module ActionController RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + module ClassMethods + # Documentation at ActionController::Renderer#render + delegate :render, to: :renderer + + # Returns a renderer class (inherited from ActionController::Renderer) + # for the controller. + def renderer + @renderer ||= Renderer.for(self) + end + end + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact @@ -68,6 +79,7 @@ module ActionController end if options.delete(:nothing) + ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") options[:body] = nil end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index b9a1e7d242..4cb634477e 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -13,9 +13,14 @@ module ActionController #:nodoc: # by including a token in the rendered HTML for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received - # token with the token in the session. Only HTML and JavaScript requests are checked, - # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). + # token with the token in the session. All requests are checked except GET requests + # as these should be idempotent. Keep in mind that all session-oriented requests + # should be CSRF protected, including JavaScript and HTML requests. + # + # 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. # # 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 @@ -26,15 +31,21 @@ module ActionController #:nodoc: # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you'll need something like: + # you're building an API you should change forgery protection method in + # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # - # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which checks the token and resets the session if it doesn't match what was expected. - # A call to this method is generated for new \Rails applications by default. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. + # By default <tt>protect_from_forgery</tt> protects your session with + # <tt>:null_session</tt> method, which provides an empty session + # during request. + # + # We may want to disable CSRF protection for APIs since they are typically + # designed to be state-less. That is, the request API client will handle + # the session for you instead of Rails. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including @@ -86,7 +97,12 @@ module ActionController #:nodoc: # Valid Options: # # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>. - # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed proc or method reference. + # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. + # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the + # callback chain. If you need to make the verification depend on other callbacks, like authentication methods + # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the + # verification callback in the position of the protect_from_forgery call. This means any callbacks added + # before are run first. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -94,9 +110,11 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) + options = options.reverse_merge(prepend: true) + self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token - prepend_before_action :verify_authenticity_token, options + before_action :verify_authenticity_token, options append_after_action :verify_same_origin_request end @@ -240,7 +258,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? @@ -268,7 +286,9 @@ module ActionController #:nodoc: # session token. Essentially the inverse of # +masked_authenticity_token+. def valid_authenticity_token?(session, encoded_masked_token) - return false if encoded_masked_token.nil? || encoded_masked_token.empty? + if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) + return false + end begin masked_token = Base64.strict_decode64(encoded_masked_token) diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 04401cad7b..af31de1f3a 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -110,9 +110,9 @@ module ActionController #:nodoc: # This means that, if you have <code>yield :title</code> in your layout # and you want to use streaming, you would have to render the whole template # (and eventually trigger all queries) before streaming the title and all - # assets, which kills the purpose of streaming. For this reason Rails 3.1 - # introduces a new helper called +provide+ that does the same as +content_for+ - # but tells the layout to stop searching for other entries and continue rendering. + # assets, which kills the purpose of streaming. For this purpose, you can use + # a helper called +provide+ that does the same as +content_for+ but tells the + # layout to stop searching for other entries and continue rendering. # # For instance, the template above using +provide+ would be: # diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 01bbd749c1..c98e937423 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/filters' -require 'active_support/deprecation' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' @@ -118,7 +117,7 @@ module ActionController self.always_permitted_parameters = %w( controller action ) def self.const_missing(const_name) - super unless const_name == :NEVER_UNPERMITTED_PARAMS + return super unless const_name == :NEVER_UNPERMITTED_PARAMS ActiveSupport::Deprecation.warn(<<-MSG.squish) `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. Use `ActionController::Parameters.always_permitted_parameters` instead. @@ -269,7 +268,7 @@ module ActionController # # params.permit(:name) # - # +:name+ passes it is a key of +params+ whose associated value is of type + # +:name+ passes if it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 572d1770f7..5a0e5c62e4 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -4,7 +4,10 @@ module ActionController # # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define # url options like the +host+. In order to do so, this module requires the host class - # to implement +env+ and +request+, which need to be a Rack-compatible. + # to implement +env+ which needs to be Rack-compatible and +request+ + # which is either an instance of +ActionDispatch::Request+ or an object + # that responds to the +host+, +optional_port+, +protocol+ and + # +symbolized_path_parameter+ methods. # # class RootUrl # include ActionController::UrlFor diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb new file mode 100644 index 0000000000..e8b29c5b5e --- /dev/null +++ b/actionpack/lib/action_controller/renderer.rb @@ -0,0 +1,100 @@ +require 'active_support/core_ext/hash/keys' + +module ActionController + # ActionController::Renderer allows to render arbitrary templates + # without requirement of being in controller actions. + # + # You get a concrete renderer class by invoking ActionController::Base#renderer. + # For example, + # + # ApplicationController.renderer + # + # It allows you to call method #render directly. + # + # ApplicationController.renderer.render template: '...' + # + # You can use a shortcut on controller to replace previous example with: + # + # ApplicationController.render template: '...' + # + # #render method allows you to use any options as when rendering in controller. + # For example, + # + # FooController.render :action, locals: { ... }, assigns: { ... } + # + # The template will be rendered in a Rack environment which is accessible through + # ActionController::Renderer#env. You can set it up in two ways: + # + # * by changing renderer defaults, like + # + # ApplicationController.renderer.defaults # => hash with default Rack environment + # + # * by initializing an instance of renderer by passing it a custom environment. + # + # ApplicationController.renderer.new(method: 'post', https: true) + # + class Renderer + class_attribute :controller, :defaults + # Rack environment to render templates in. + attr_reader :env + + class << self + delegate :render, to: :new + + # Create a new renderer class for a specific controller class. + def for(controller) + Class.new self do + self.controller = controller + self.defaults = { + http_host: 'example.org', + https: false, + method: 'get', + script_name: '', + 'rack.input' => '' + } + end + end + end + + # Accepts a custom Rack environment to render templates in. + # It will be merged with ActionController::Renderer.defaults + def initialize(env = {}) + @env = normalize_keys(defaults).merge normalize_keys(env) + @env['action_dispatch.routes'] = controller._routes + end + + # Render templates with any options from ActionController::Base#render_to_string. + def render(*args) + raise 'missing controller' unless controller? + + instance = controller.build_with_env(env) + instance.render_to_string(*args) + end + + private + def normalize_keys(env) + http_header_format(env).tap do |new_env| + handle_method_key! new_env + handle_https_key! new_env + end + end + + def http_header_format(env) + env.transform_keys do |key| + key.is_a?(Symbol) ? key.to_s.upcase : key + end + end + + def handle_method_key!(env) + if method = env.delete('METHOD') + env['REQUEST_METHOD'] = method.upcase + end + end + + def handle_https_key!(env) + if env.has_key? 'HTTPS' + env['HTTPS'] = env['HTTPS'] ? 'on' : 'off' + end + end + end +end diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb new file mode 100644 index 0000000000..0179f4afcd --- /dev/null +++ b/actionpack/lib/action_controller/template_assertions.rb @@ -0,0 +1,9 @@ +module ActionController + module TemplateAssertions + def assert_template(options = {}, message = nil) + 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 d30615fade..96f161fb09 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -2,197 +2,10 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/hash/keys' - +require 'action_controller/template_assertions' require 'rails-dom-testing' 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 - end - end - class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' @@ -201,13 +14,14 @@ module ActionController super self.session = TestSession.new - self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => SecureRandom.hex(16)) + self.session_options = TestSession::DEFAULT_OPTIONS end def assign_parameters(routes, controller_path, action, parameters = {}) - parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) - extra_keys = routes.extra_keys(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 + parameters.each do |key, value| if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) value = value.map{ |v| v.duplicable? ? v.dup : v } @@ -217,7 +31,7 @@ module ActionController value = value.dup end - if extra_keys.include?(key) + if extra_keys.include?(key) || key == :action || key == :controller non_path_parameters[key] = value else if value.is_a?(Array) @@ -230,19 +44,16 @@ 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") # Clear the filter cache variables so they're not stale @filtered_parameters = @filtered_env = @filtered_path = nil - params = self.request_parameters.dup - %w(controller action only_path).each do |k| - params.delete(k) - params.delete(k.to_sym) - end - data = params.to_query - + data = request_parameters.to_query @env['CONTENT_LENGTH'] = data.length.to_s @env['rack.input'] = StringIO.new(data) end @@ -354,7 +165,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. @@ -407,21 +218,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 @@ -494,55 +299,66 @@ module ActionController # Simulate a GET request with the given parameters. # # - +action+: The controller action to call. - # - +parameters+: The HTTP parameters that you want to pass. This may - # be +nil+, a hash, or a string that is appropriately encoded + # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. + # - +body+: The request body with a string that is appropriately encoded # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). # - +session+: A hash of parameters to store in the session. This may be +nil+. # - +flash+: A hash of parameters to store in the flash. This may be +nil+. # # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with # +post+, +patch+, +put+, +delete+, and +head+. + # Example sending parameters, session and setting a flash message: + # + # get :show, + # params: { id: 7 }, + # session: { user_id: 1 }, + # flash: { notice: 'This is flash message' } # # Note that the request method is not verified. The different methods are # available to make the tests more expressive. def get(action, *args) - process(action, "GET", *args) + process_with_kwargs("GET", action, *args) end # Simulate a POST request with the given parameters and set/volley the response. # See +get+ for more details. def post(action, *args) - process(action, "POST", *args) + process_with_kwargs("POST", action, *args) end # Simulate a PATCH request with the given parameters and set/volley the response. # See +get+ for more details. def patch(action, *args) - process(action, "PATCH", *args) + process_with_kwargs("PATCH", action, *args) end # Simulate a PUT request with the given parameters and set/volley the response. # See +get+ for more details. def put(action, *args) - process(action, "PUT", *args) + process_with_kwargs("PUT", action, *args) end # Simulate a DELETE request with the given parameters and set/volley the response. # See +get+ for more details. def delete(action, *args) - process(action, "DELETE", *args) + process_with_kwargs("DELETE", action, *args) end # Simulate a HEAD request with the given parameters and set/volley the response. # See +get+ for more details. def head(action, *args) - process(action, "HEAD", *args) + process_with_kwargs("HEAD", action, *args) end - def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) + def xml_http_request(*args) + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + xhr and xml_http_request methods are deprecated in favor of + `get :index, xhr: true` and `post :create, xhr: true` + MSG + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - __send__(request_method, action, parameters, session, flash).tap do + @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + __send__(*args).tap do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end @@ -566,41 +382,69 @@ module ActionController # parameters and set/volley the response. # # - +action+: The controller action to call. - # - +http_method+: Request method used to send the http request. Possible values - # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. - # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a - # string that is appropriately encoded (+application/x-www-form-urlencoded+ - # or +multipart/form-data+). + # - +method+: Request method used to send the HTTP request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol. + # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. + # - +body+: The request body with a string that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). # - +session+: A hash of parameters to store in the session. This may be +nil+. # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # - +format+: Request format. Defaults to +nil+. Can be string or symbol. # # Example calling +create+ action and sending two params: # - # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' } - # - # Example sending parameters, +nil+ session and setting a flash message: - # - # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' } + # process :create, + # method: 'POST', + # params: { + # user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # }, + # session: { user_id: 1 }, + # flash: { notice: 'This is flash message' } # # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests # prefer using #get, #post, #patch, #put, #delete and #head methods # respectively which will make tests more expressive. # # Note that the request method is not verified. - def process(action, http_method = 'GET', *args) + def process(action, *args) check_required_ivars - if args.first.is_a?(String) && http_method != 'HEAD' - @request.env['RAW_POST_DATA'] = args.shift + if kwarg_request?(args) + parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr) + else + http_method, parameters, session, flash = args + format = nil + + if parameters.is_a?(String) && http_method != 'HEAD' + body = parameters + parameters = nil + end + + if parameters.present? || session.present? || flash.present? + non_kwarg_request_warning + end + end + + if body.present? + @request.env['RAW_POST_DATA'] = body + end + + if http_method.present? + http_method = http_method.to_s.upcase + else + http_method = "GET" end - parameters, session, flash = args 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 + @html_document = nil unless @controller.respond_to?(:recycle!) @@ -622,15 +466,18 @@ module ActionController @request.session.update(session) if session @request.flash.update(flash || {}) + if xhr + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + end + @controller.request = @request @controller.response = @response - build_request_uri(action, parameters) - - name = @request.parameters[:action] + build_request_uri(controller_class_name, action, parameters) @controller.recycle! - @controller.process(name) + @controller.process(action) if cookies = @request.env['action_dispatch.cookies'] unless @response.committed? @@ -639,10 +486,15 @@ module ActionController 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 + @request.session.delete('flash') + end + + if xhr + @request.env.delete 'HTTP_X_REQUESTED_WITH' + @request.env.delete 'HTTP_ACCEPT' end @response @@ -693,6 +545,38 @@ module ActionController private + def process_with_kwargs(http_method, action, *args) + if kwarg_request?(args) + args.first.merge!(method: http_method) + process(action, *args) + else + non_kwarg_request_warning if args.any? + + args = args.unshift(http_method) + process(action, *args) + end + end + + REQUEST_KWARGS = %i(params session flash method body xhr) + def kwarg_request?(args) + args[0].respond_to?(:keys) && ( + (args[0].key?(:format) && args[0].keys.size == 1) || + args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } + ) + end + + def non_kwarg_request_warning + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + ActionController::TestCase HTTP request methods will accept only + keyword arguments in future Rails versions. + + Examples: + + get :show, params: { id: 1 }, session: { user_id: 1 } + process :update, method: :post, params: { id: 1 } + MSG + end + def document_root_element html_document.root end @@ -707,10 +591,11 @@ module ActionController end end - def build_request_uri(action, parameters) + 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) diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 63a3cbc90b..747d295261 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -69,17 +69,17 @@ module ActionDispatch end def date - if date_header = headers['Date'] + if date_header = headers[DATE] Time.httpdate(date_header) end end def date? - headers.include?('Date') + headers.include?(DATE) end def date=(utc_time) - headers['Date'] = utc_time.httpdate + headers[DATE] = utc_time.httpdate end def etag=(etag) @@ -89,6 +89,7 @@ module ActionDispatch private + DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 2b851cc28d..3170389b36 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -16,7 +16,7 @@ module ActionDispatch # env["action_dispatch.parameter_filter"] = [:foo, "bar"] # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" # - # env["action_dispatch.parameter_filter"] = lambda do |k,v| + # env["action_dispatch.parameter_filter"] = -> (k, v) do # v.reverse! if k =~ /secret/i # end # => reverses the value to all keys matching /secret/i diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index cd603649c3..bf79963351 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -4,7 +4,7 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: - def filtered_location + def filtered_location # :nodoc: filters = location_filter if !filters.empty? && location_filter_match?(filters) FILTERED diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 53a98c5d0a..ff336b7354 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,8 +10,6 @@ module ActionDispatch self.ignore_accept_header = false end - attr_reader :variant - # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -75,18 +73,22 @@ module ActionDispatch # Sets the \variant for template. def variant=(variant) - if variant.is_a?(Symbol) - @variant = [variant] - elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } - @variant = variant + variant = Array(variant) + + if variant.all? { |v| v.is_a?(Symbol) } + @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" end end + def variant + @variant ||= ActiveSupport::ArrayInquirer.new + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 047a17937a..7e585aa244 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["text/calendar"] } + # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.xml { render xml: @post } # end # end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 0e4da36038..01a10c693b 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -27,7 +27,7 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form # http://www.ietf.org/rfc/rfc4627.txt # http://www.json.org/JSONRequest.html -Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) +Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json ) Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index cadbfc88cb..3c62c055e5 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -20,6 +20,8 @@ module ActionDispatch include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL + HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc: + autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' @@ -50,7 +52,6 @@ module ActionDispatch @original_fullpath = nil @fullpath = nil @ip = nil - @uuid = nil end def check_path_parameters! @@ -114,7 +115,7 @@ module ActionDispatch end def engine_script_name(_routes) # :nodoc: - env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"] + env[_routes.env_key] end def request_method=(request_method) #:nodoc: @@ -140,47 +141,11 @@ module ActionDispatch HTTP_METHOD_LOOKUP[method] end - # Is this a GET (or HEAD) request? - # Equivalent to <tt>request.request_method_symbol == :get</tt>. - def get? - HTTP_METHOD_LOOKUP[request_method] == :get - end - - # Is this a POST request? - # Equivalent to <tt>request.request_method_symbol == :post</tt>. - def post? - HTTP_METHOD_LOOKUP[request_method] == :post - end - - # Is this a PATCH request? - # Equivalent to <tt>request.request_method == :patch</tt>. - def patch? - HTTP_METHOD_LOOKUP[request_method] == :patch - end - - # Is this a PUT request? - # Equivalent to <tt>request.request_method_symbol == :put</tt>. - def put? - HTTP_METHOD_LOOKUP[request_method] == :put - end - - # Is this a DELETE request? - # Equivalent to <tt>request.request_method_symbol == :delete</tt>. - def delete? - HTTP_METHOD_LOOKUP[request_method] == :delete - end - - # Is this a HEAD request? - # Equivalent to <tt>request.request_method_symbol == :head</tt>. - def head? - HTTP_METHOD_LOOKUP[request_method] == :head - end - # Provides access to the request's HTTP headers, for example: # # request.headers["Content-Type"] # => "text/plain" def headers - Http::Headers.new(@env) + @headers ||= Http::Headers.new(@env) end # Returns a +String+ with the last requested path including their params. @@ -234,23 +199,37 @@ module ActionDispatch end alias :xhr? :xml_http_request? + # Returns the IP address of client as a +String+. def ip @ip ||= super end - # Originating IP address, usually set by the RemoteIp middleware. + # Returns the IP address of client as a +String+, + # usually set by the RemoteIp middleware. def remote_ip @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s 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 # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). # # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. # This relies on the rack variable set by the ActionDispatch::RequestId middleware. - def uuid - @uuid ||= env["action_dispatch.request_id"] + def request_id + env[ACTION_DISPATCH_REQUEST_ID] + end + + def request_id=(id) # :nodoc: + env[ACTION_DISPATCH_REQUEST_ID] = id + end + + alias_method :uuid, :request_id + + def x_request_id # :nodoc: + @env[HTTP_X_REQUEST_ID] end # Returns the lowercase name of the HTTP server software. @@ -280,6 +259,8 @@ module ActionDispatch end end + # 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) end @@ -338,7 +319,7 @@ module ActionDispatch end protected - def parse_query(qs) + def parse_query(*) Utils.deep_munge(super) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 4061ea71a3..9e53a0f08b 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -113,10 +113,10 @@ module ActionDispatch # :nodoc: # The underlying body, as a streamable object. attr_reader :stream - def initialize(status = 200, header = {}, body = []) + def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers) super() - header = merge_default_headers(header, self.class.default_headers) + header = merge_default_headers(header, default_headers) self.body, self.header, self.status = body, header, status @@ -274,7 +274,7 @@ module ActionDispatch # :nodoc: end # Turns the Response into a Rack-compatible array of the status, headers, - # and body. Allows explict splatting: + # and body. Allows explicit splatting: # # status, headers, body = *response def to_a @@ -308,9 +308,7 @@ module ActionDispatch # :nodoc: end def merge_default_headers(original, default) - return original unless default.respond_to?(:merge) - - default.merge(original) + default.respond_to?(:merge) ? default.merge(original) : original end def build_buffer(response, body) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 001b14ec97..f5b709ccd6 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -67,7 +67,7 @@ module ActionDispatch end def path_for(options) - path = options[:script_name].to_s.chomp("/") + path = options[:script_name].to_s.chomp("/".freeze) path << options[:path] if options.key?(:path) add_trailing_slash(path) if options[:trailing_slash] @@ -229,7 +229,7 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.raw_host_with_port # => "example.com:8080" def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"] + if forwarded = env["HTTP_X_FORWARDED_HOST"].presence forwarded.split(/,\s?/).last else env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 992c1a9efe..c0566c6fc9 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -39,7 +39,7 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{Hash[constraints.sort].inspect}" + 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? raise ActionController::UrlGenerationError, message diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 4d5c18984a..cbc985640a 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -11,7 +11,7 @@ module ActionDispatch ## # +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) @name = name @app = app @path = path @@ -19,6 +19,7 @@ module ActionDispatch @constraints = constraints @defaults = defaults @required_defaults = nil + @_required_defaults = required_defaults || [] @required_parts = nil @parts = nil @decorated_ast = nil @@ -36,7 +37,7 @@ module ActionDispatch def requirements # :nodoc: # needed for rails `rake routes` - path.requirements.merge(@defaults).delete_if { |_,v| + @defaults.merge(path.requirements).delete_if { |_,v| /.+?/ == v } end @@ -73,7 +74,7 @@ module ActionDispatch end def required_default?(key) - (constraints[:required_defaults] || []).include?(key) + @_required_defaults.include?(key) end def required_defaults @@ -92,8 +93,6 @@ module ActionDispatch def matches?(request) constraints.all? do |method, value| - next true unless request.respond_to?(method) - case value when Regexp, String value === request.send(method).to_s diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 2b036796ab..cc4bd6105d 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -88,7 +88,7 @@ module ActionDispatch end def custom_routes - partitioned_routes.last + routes.custom_routes end def filter_routes(path) diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 2b0a6575d4..d02ed96d0d 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -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 80e3818ccd..5990964b57 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -5,16 +5,21 @@ module ActionDispatch class Routes # :nodoc: include Enumerable - attr_reader :routes, :named_routes + attr_reader :routes, :named_routes, :custom_routes, :anchored_routes def initialize @routes = [] @named_routes = {} @ast = nil - @partitioned_routes = nil + @anchored_routes = [] + @custom_routes = [] @simulator = nil end + def empty? + routes.empty? + end + def length routes.length end @@ -30,18 +35,22 @@ module ActionDispatch def clear routes.clear + anchored_routes.clear + custom_routes.clear named_routes.clear end - def partitioned_routes - @partitioned_routes ||= routes.partition do |r| - r.path.anchored && r.ast.grep(Nodes::Symbol).all?(&:default_regexp?) + def partition_route(route) + if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?) + anchored_routes << route + else + custom_routes << route end end def ast @ast ||= begin - asts = partitioned_routes.first.map(&:ast) + asts = anchored_routes.map(&:ast) Nodes::Or.new(asts) unless asts.empty? end end @@ -54,12 +63,13 @@ module ActionDispatch 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) + def add_route(app, path, conditions, required_defaults, defaults, name = nil) + route = Route.new(name, app, path, conditions, required_defaults, defaults) route.precedence = routes.length routes << route named_routes[name] = route if name && !named_routes[name] + partition_route(route) clear_cache! route end @@ -68,7 +78,6 @@ module ActionDispatch def clear_cache! @ast = nil - @partitioned_routes = nil @simulator = nil end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 8d3ce24612..dd1f140051 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -73,12 +73,15 @@ module ActionDispatch # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies. # - # domain: nil # Does not sets cookie domain. (default) + # domain: nil # Does not set cookie domain. (default) # domain: :all # Allow the cookie for the top most level # # domain and subdomains. # domain: %w(.example.com .example.org) # Allow the cookie # # for concrete domain names. # + # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly + # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. @@ -181,7 +184,7 @@ module ActionDispatch # to the Message{Encryptor,Verifier} allows us to handle the # (de)serialization step within the cookie jar, which gives us the # opportunity to detect and migrate legacy cookies. - module VerifyAndUpgradeLegacySignedMessage + module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) @@ -392,7 +395,7 @@ module ActionDispatch end end - class JsonSerializer + class JsonSerializer # :nodoc: def self.load(value) ActiveSupport::JSON.decode(value) end @@ -402,7 +405,7 @@ module ActionDispatch end end - module SerializedCookieJars + module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze protected @@ -454,12 +457,16 @@ module ActionDispatch @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end + # Returns the value of the cookie by +name+ if it is untampered, + # returns +nil+ otherwise or if no such cookie exists. def [](name) if signed_message = @parent_jar[name] deserialize name, verify(signed_message) end end + # Signs and sets the cookie named +name+. The second argument may be the cookie's + # value or a hash of options as documented above. def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! @@ -482,8 +489,8 @@ module ActionDispatch # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if # secrets.secret_token and secrets.secret_key_base are both set. It reads - # legacy cookies signed with the old dummy key generator and re-saves - # them using the new key generator to provide a smooth upgrade path. + # legacy cookies signed with the old dummy key generator and signs and + # re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage @@ -511,12 +518,16 @@ module ActionDispatch @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end + # Returns the value of the cookie by +name+ if it is untampered, + # returns +nil+ otherwise or if no such cookie exists. def [](name) if encrypted_message = @parent_jar[name] deserialize name, decrypt_and_verify(encrypted_message) end end + # Encrypts and sets the cookie named +name+. The second argument may be the cookie's + # value or a hash of options as documented above. def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index a4862e33aa..d176a73633 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,6 @@ require 'action_controller/metal/exceptions' require 'active_support/core_ext/module/attribute_accessors' +require 'rack/utils' module ActionDispatch class ExceptionWrapper @@ -87,8 +88,7 @@ module ActionDispatch def source_extracts backtrace.map do |trace| - file, line = trace.split(":") - line_number = line.to_i + file, line_number = extract_file_and_line_number(trace) { code: source_fragment(file, line_number), @@ -139,6 +139,13 @@ module ActionDispatch end end + def extract_file_and_line_number(trace) + # Split by the first colon followed by some digits, which works for both + # Windows and Unix path styles. + file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace + [file, line.to_i] + end + def expand_backtrace @exception.backtrace.unshift( @exception.to_s.split("\n") diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index a7f95150a4..59639a010e 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -80,24 +80,30 @@ module ActionDispatch include Enumerable def self.from_session_value(value) #:nodoc: - flash = case value - when FlashHash # Rails 3.1, 3.2 - new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used)) - when Hash # Rails 4.0 - new(value['flashes'], value['discard']) - else - new - end - - flash.tap(&:sweep) - end - - # Builds a hash containing the discarded values and the hashes - # representing the flashes. - # If there are no values in @flashes, returns nil. + case value + when FlashHash # Rails 3.1, 3.2 + flashes = value.instance_variable_get(:@flashes) + if discard = value.instance_variable_get(:@used) + flashes.except!(*discard) + end + new(flashes, flashes.keys) + when Hash # Rails 4.0 + flashes = value['flashes'] + if discard = value['discard'] + flashes.except!(*discard) + end + new(flashes, flashes.keys) + else + new + end + end + + # Builds a hash containing the flashes to keep for the next request. + # If there are none to keep, returns nil. def to_session_value #:nodoc: - return nil if empty? - {'discard' => @discard.to_a, 'flashes' => @flashes} + flashes_to_keep = @flashes.except(*@discard) + return nil if flashes_to_keep.empty? + {'flashes' => flashes_to_keep} end def initialize(flashes = {}, discard = []) #:nodoc: diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 040cb215b7..7cde76b30e 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,10 +17,10 @@ module ActionDispatch end def call(env) - status = env["PATH_INFO"][1..-1] + status = env["PATH_INFO"][1..-1].to_i request = ActionDispatch::Request.new(env) content_type = request.formats.first - body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) } + body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } render(status, content_type, body) end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 15b5a48535..6c7fba00cb 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -11,9 +11,9 @@ module ActionDispatch # the response body. This is important for streaming responses such as the # following: # - # self.response_body = lambda { |response, output| + # self.response_body = -> (response, output) do # # code here which refers to application models - # } + # end # # Cleanup callbacks will not be called until after the response_body lambda # is evaluated, ensuring that it can refer to application models and other diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 7c4236518d..9f894e2ec6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -74,7 +74,7 @@ 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) + env["action_dispatch.remote_ip"] = GetIp.new(env, check_ip, proxies) @app.call(env) end @@ -82,10 +82,10 @@ module ActionDispatch # 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) + def initialize(env, check_ip, proxies) @env = env - @check_ip = middleware.check_ip - @proxies = middleware.proxies + @check_ip = check_ip + @proxies = proxies end # Sort through the various IP address headers, looking for the IP most diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 25658bac3d..1555ff72af 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/string/access' module ActionDispatch # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through - # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. + # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header. # # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the @@ -12,19 +12,24 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId + X_REQUEST_ID = "X-Request-Id".freeze # :nodoc: + def initialize(app) @app = app end def call(env) - env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id - @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] } + req = ActionDispatch::Request.new env + req.request_id = make_request_id(req.x_request_id) + @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id } end private - def external_request_id(env) - if request_id = env["HTTP_X_REQUEST_ID"].presence - request_id.gsub(/[^\w\-]/, "").first(255) + def make_request_id(request_id) + if request_id.presence + request_id.gsub(/[^\w\-]/, "".freeze).first(255) + else + internal_request_id end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 625050dc4b..857e49a682 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -2,12 +2,15 @@ require 'action_dispatch/middleware/session/abstract_store' module ActionDispatch module Session - # Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful + # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful # if you don't store critical data in your sessions and you don't need them to live for extended periods # of time. + # + # ==== Options + # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used. + # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring. + # By default, the <tt>:expires_in</tt> option of the cache is used. class CacheStore < AbstractStore - # Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is - # not specified, <tt>Rails.cache</tt> will be used. def initialize(app, options = {}) @cache = options[:cache] || Rails.cache options[:expire_after] ||= @cache.options[:expires_in] diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index ed25c67ae5..d8f9614904 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -52,6 +52,16 @@ module ActionDispatch # JavaScript before upgrading. # # Note that changing the secret key will invalidate all existing sessions! + # + # Because CookieStore extends Rack::Session::Abstract::ID, many of the + # options described there can be used to customize the session cookie that + # is generated. For example: + # + # Rails.application.config.session_store :cookie_store, expire_after: 14.days + # + # would set the session cookie to expire automatically 14 days after creation. + # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and + # <tt>:httponly</tt>. class CookieStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index b4d6629c35..cb19786f0b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -8,6 +8,10 @@ end module ActionDispatch module Session + # A session store that uses MemCache to implement storage. + # + # ==== Options + # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring. class MemCacheStore < Rack::Session::Dalli include Compatibility include StaleSessionCheck diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 002bf8b11a..b098ea389f 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,33 +3,39 @@ require 'active_support/core_ext/uri' module ActionDispatch # This middleware returns a file's contents from disk in the body response. - # When initialized it can accept an optional 'Cache-Control' header which + # When initialized, it can accept an optional 'Cache-Control' header, which # will be set when a response containing a file's contents is delivered. # # This middleware will render the file specified in `env["PATH_INFO"]` - # where the base path is in the +root+ directory. For example if the +root+ - # is set to `public/` then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with contents of a file + # where the base path is in the +root+ directory. For example, if the +root+ + # is set to `public/`, then a request with `env["PATH_INFO"]` of + # `assets/application.js` will return a response with the contents of a file # 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. + # does not exist, a 404 "File not Found" response will be returned. class FileHandler - def initialize(root, cache_control) + def initialize(root, cache_control, index: 'index') @root = root.chomp('/') @compiled_root = /^#{Regexp.escape(root)}/ headers = cache_control && { 'Cache-Control' => cache_control } @file_server = ::Rack::File.new(@root, headers) + @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. + # + # Used by the `Static` class to check the existence of a valid file + # in the server's `public/` directory (see Static#call). def match?(path) path = URI.parser.unescape(path) return false unless path.valid_encoding? + path = Rack::Utils.clean_path_info path - paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v| - Rack::Utils.clean_path_info v - } + paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p) + path = File.join(@root, p.force_encoding('UTF-8')) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -48,6 +54,9 @@ module ActionDispatch if gzip_path && gzip_encoding_accepted?(env) env['PATH_INFO'] = gzip_path status, headers, body = @file_server.call(env) + if status == 304 + return [status, headers, body] + end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else @@ -86,7 +95,7 @@ module ActionDispatch end # This middleware will attempt to return the contents of a file's body from - # disk in the response. If a file is not found on disk, the request will be + # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized # to serve assets from a server's `public/` directory. # @@ -95,9 +104,9 @@ 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) + def initialize(app, path, cache_control = nil, index: 'index') @app = app - @file_handler = FileHandler.new(path, cache_control) + @file_handler = FileHandler.new(path, cache_control, index: index) end def call(env) @@ -105,7 +114,7 @@ module ActionDispatch when 'GET', 'HEAD' path = env['PATH_INFO'].chomp('/') if match = @file_handler.match?(path) - env["PATH_INFO"] = match + env['PATH_INFO'] = match return @file_handler.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb index 24e44f31ac..6e995c85c1 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb @@ -4,13 +4,13 @@ <%= route[:name] %><span class='helper'>_path</span> <% end %> </td> - <td data-route-verb='<%= route[:verb] %>'> + <td> <%= route[:verb] %> </td> - <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'> + <td data-route-path='<%= route[:path] %>'> <%= route[:path] %> </td> - <td data-route-reqs='<%= route[:reqs] %>'> - <%= route[:reqs] %> + <td> + <%=simple_format route[:reqs] %> </td> </tr> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 5cee0b5932..429ea7057c 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -81,92 +81,87 @@ </table> <script type='text/javascript'> - // Iterates each element through a function - function each(elems, func) { - if (!elems instanceof Array) { elems = [elems]; } - for (var i = 0, len = elems.length; i < len; i++) { - func(elems[i]); - } - } - - // Sets innerHTML for an element - function setContent(elem, text) { - elem.innerHTML = text; - } + // support forEarch iterator on NodeList + NodeList.prototype.forEach = Array.prototype.forEach; // Enables path search functionality function setupMatchPaths() { - // Check if the user input (sanitized as a path) matches the regexp data attribute - function checkExactMatch(section, elem, value) { - var string = sanitizePath(value), - regexp = elem.getAttribute("data-regexp"); - - showMatch(string, regexp, section, elem); + // Check if there are any matched results in a section + function checkNoMatch(section, noMatchText) { + if (section.children.length <= 1) { + section.innerHTML += noMatchText; + } } - // Check if the route path data attribute contains the user input - function checkFuzzyMatch(section, elem, value) { - var string = elem.getAttribute("data-route-path"), - regexp = value; - - showMatch(string, regexp, section, elem); + // get JSON from url and invoke callback with result + function getJSON(url, success) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = function() { + if (this.status == 200) + success(JSON.parse(this.response)); + }; + xhr.send(); } - // Display the parent <tr> element in the appropriate section when there's a match - function showMatch(string, regexp, section, elem) { - if(string.match(RegExp(regexp))) { - section.appendChild(elem.parentNode.cloneNode(true)); + function delayedKeyup(input, callback) { + var timeout; + input.onkeyup = function(){ + if (timeout) clearTimeout(timeout); + timeout = setTimeout(callback, 300); } } - // Check if there are any matched results in a section - function checkNoMatch(section, defaultText, noMatchText) { - if (section.innerHTML === defaultText) { - setContent(section, defaultText + noMatchText); - } - } - - // Ensure path always starts with a slash "/" and remove params or fragments + // remove params or fragments function sanitizePath(path) { - var path = path.charAt(0) == '/' ? path : "/" + path; - return path.replace(/\#.*|\?.*/, ''); + return path.replace(/[#?].*/, ''); } - var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), - searchElem = document.querySelector('#search'), - exactMatches = document.querySelector('#exact_matches'), - fuzzyMatches = document.querySelector('#fuzzy_matches'); + var pathElements = document.querySelectorAll('#route_table [data-route-path]'), + searchElem = document.querySelector('#search'), + exactSection = document.querySelector('#exact_matches'), + fuzzySection = document.querySelector('#fuzzy_matches'); // Remove matches when no search value is present searchElem.onblur = function(e) { if (searchElem.value === "") { - setContent(exactMatches, ""); - setContent(fuzzyMatches, ""); + exactSection.innerHTML = ""; + fuzzySection.innerHTML = ""; } } // On key press perform a search for matching paths - searchElem.onkeyup = function(e){ - var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', + delayedKeyup(searchElem, function() { + var path = sanitizePath(searchElem.value), + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; - // Clear out results section - setContent(exactMatches, defaultExactMatch); - setContent(fuzzyMatches, defaultFuzzyMatch); + if (!path) + return searchElem.onblur(); - // Display exact matches and fuzzy matches - each(regexpElems, function(elem) { - checkExactMatch(exactMatches, elem, userInput); - checkFuzzyMatch(fuzzyMatches, elem, userInput); - }) + getJSON('/rails/info/routes?path=' + path, function(matches){ + // Clear out results section + exactSection.innerHTML = defaultExactMatch; + fuzzySection.innerHTML = defaultFuzzyMatch; - // Display 'No Matches' message when no matches are found - checkNoMatch(exactMatches, defaultExactMatch, noExactMatch); - checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch); - } + // Display exact matches and fuzzy matches + pathElements.forEach(function(elem) { + var elemPath = elem.getAttribute('data-route-path'); + + if (matches['exact'].indexOf(elemPath) != -1) + exactSection.appendChild(elem.parentNode.cloneNode(true)); + + if (matches['fuzzy'].indexOf(elemPath) != -1) + fuzzySection.appendChild(elem.parentNode.cloneNode(true)); + }) + + // Display 'No Matches' message when no matches are found + checkNoMatch(exactSection, noExactMatch); + checkNoMatch(fuzzySection, noFuzzyMatch); + }) + }) } // Enables functionality to toggle between `_path` and `_url` helper suffixes @@ -174,19 +169,20 @@ // Sets content for each element function setValOn(elems, val) { - each(elems, function(elem) { - setContent(elem, val); + elems.forEach(function(elem) { + elem.innerHTML = val; }); } // Sets onClick event for each element function onClick(elems, func) { - each(elems, function(elem) { + elems.forEach(function(elem) { elem.onclick = func; }); } var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); + onClick(toggleLinks, function(){ var helperTxt = this.getAttribute("data-route-helper"), helperElems = document.querySelectorAll('[data-route-name] span.helper'); diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 973627f106..773d41de88 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -9,7 +9,8 @@ module ActionDispatch # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new - + + # Creates a session hash, merging the properties of the previous session if any def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -85,11 +86,14 @@ module ActionDispatch 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) @@ -97,39 +101,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/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/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index df5ebb6751..48c10a7d4c 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -28,23 +28,6 @@ module ActionDispatch super.to_s end - def regexp - __getobj__.path.to_regexp - end - - def json_regexp - str = regexp.inspect. - sub('\\A' , '^'). - sub('\\Z' , '$'). - sub('\\z' , '$'). - sub(/^\// , ''). - sub(/\/[a-z]*$/ , ''). - gsub(/\(\?#.+\)/ , ''). - gsub(/\(\?-\w+:/ , '('). - gsub(/\s/ , '') - Regexp.new(str).source - end - def reqs @reqs ||= begin reqs = endpoint @@ -62,7 +45,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} end def engine? @@ -117,11 +100,10 @@ module ActionDispatch end.reject(&:internal?).collect do |route| collect_engine_routes(route) - { name: route.name, - verb: route.verb, - path: route.path, - reqs: route.reqs, - regexp: route.json_regexp } + { name: route.name, + verb: route.verb, + path: route.path, + reqs: route.reqs } end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8b04dfaa45..7cfe4693c1 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -418,7 +418,7 @@ module ActionDispatch # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints # match 'photos/:id', to: PhotosController.action(:show), via: :get @@ -470,7 +470,7 @@ module ActionDispatch # +call+ or a string representing a controller's action. # # match 'path', to: 'controller#action', via: :get - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get # match 'path', to: RackApp, via: :get # # [:on] @@ -806,7 +806,7 @@ module ActionDispatch # 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 @@ -899,7 +899,7 @@ module ActionDispatch # # Requests to routes can be constrained based on specific criteria: # - # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do # resources :iphones # end # @@ -1042,7 +1042,7 @@ module ActionDispatch class Resource #:nodoc: attr_reader :controller, :path, :options, :param - def initialize(entities, options = {}) + def initialize(entities, api_only = false, options = {}) @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @@ -1050,10 +1050,15 @@ module ActionDispatch @param = (options[:param] || :id).to_sym @options = options @shallow = false + @api_only = api_only 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 @@ -1120,7 +1125,7 @@ module ActionDispatch end class SingletonResource < Resource #:nodoc: - def initialize(entities, options) + def initialize(entities, api_only, options) super @as = nil @controller = (options[:controller] || plural).to_s @@ -1128,7 +1133,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 @@ -1178,7 +1187,7 @@ module ActionDispatch return self end - resource_scope(:resource, SingletonResource.new(resources.pop, options)) do + resource_scope(:resource, SingletonResource.new(resources.pop, api_only?, options)) do yield if block_given? concerns(options[:concerns]) if options[:concerns] @@ -1336,7 +1345,7 @@ module ActionDispatch return self end - resource_scope(:resources, Resource.new(resources.pop, options)) do + resource_scope(:resources, Resource.new(resources.pop, api_only?, options)) do yield if block_given? concerns(options[:concerns]) if options[:concerns] @@ -1450,9 +1459,12 @@ module ActionDispatch parent_resource.instance_of?(Resource) && @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 @@ -1504,7 +1516,7 @@ module ActionDispatch end def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$} + path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$} end def decomposed_match(path, options) # :nodoc: @@ -1678,7 +1690,7 @@ module ActionDispatch end def shallow_nesting_depth #:nodoc: - @nesting.select(&:shallow?).size + @nesting.count(&:shallow?) end def param_constraint? #:nodoc: @@ -1739,9 +1751,10 @@ module ActionDispatch member_name = parent_resource.member_name end - name = @scope.action_name(name_prefix, prefix, collection_name, member_name) + action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name) + candidate = action_name.select(&:present?).join('_') - if candidate = name.compact.join("_").presence + unless candidate.empty? # If a name was not explicitly given, we check if it is valid # and return nil in case it isn't. Otherwise, we pass the invalid name # forward so the underlying router engine treats it and raises an exception. @@ -1764,6 +1777,10 @@ module ActionDispatch delete :destroy if parent_resource.actions.include?(:destroy) end end + + def api_only? + @set.api_only? + end end # Routing Concerns allow you to declare common routes that can be reused diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 2e116ea9cd..9934f5547a 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -247,14 +247,12 @@ module ActionDispatch args = [] model = record.to_model - name = if model.persisted? - args << model - model.model_name.singular_route_key - else - @key_strategy.call model.model_name - end - - named_route = prefix + "#{name}_#{suffix}" + named_route = if model.persisted? + args << model + get_method_for_string model.model_name.singular_route_key + else + get_method_for_class model + end [named_route, args] end @@ -309,11 +307,11 @@ module ActionDispatch def get_method_for_class(klass) name = @key_strategy.call klass.model_name - prefix + "#{name}_#{suffix}" + get_method_for_string name end def get_method_for_string(str) - prefix + "#{str}_#{suffix}" + "#{prefix}#{str}_#{suffix}" end [nil, 'new', 'edit'].each do |action| diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b4c861d306..454593b59f 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -12,14 +12,15 @@ require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing - class RouteSet #:nodoc: + # :stopdoc: + class RouteSet # Since the router holds references to many parts of the system # like engines, controllers and the application itself, inspecting # the route set can actually be really slow, therefore we default # alias inspect to to_s. alias inspect to_s - class Dispatcher < Routing::Endpoint #:nodoc: + class Dispatcher < Routing::Endpoint def initialize(defaults) @defaults = defaults @controller_class_names = ThreadSafe::Cache.new @@ -84,7 +85,7 @@ module ActionDispatch # A NamedRouteCollection instance is a collection of named routes, and also # maintains an anonymous module that can be used to install helpers for the # named routes. - class NamedRouteCollection #:nodoc: + class NamedRouteCollection include Enumerable attr_reader :routes, :url_helpers_module, :path_helpers_module @@ -161,7 +162,7 @@ module ActionDispatch routes.length end - class UrlHelper # :nodoc: + class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) OptimizedUrlHelper.new(route, options, route_name, url_strategy) @@ -176,7 +177,7 @@ module ActionDispatch attr_reader :url_strategy, :route_name - class OptimizedUrlHelper < UrlHelper # :nodoc: + class OptimizedUrlHelper < UrlHelper attr_reader :arg_size def initialize(route, options, route_name, url_strategy) @@ -198,12 +199,9 @@ module ActionDispatch private def optimized_helper(args) - params = parameterize_args(args) - missing_keys = missing_keys(params) - - unless missing_keys.empty? - raise_generation_error(params, missing_keys) - end + params = parameterize_args(args) { |k| + raise_generation_error(args) + } @route.format params end @@ -214,16 +212,21 @@ module ActionDispatch def parameterize_args(args) params = {} - @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + @arg_size.times { |i| + key = @required_parts[i] + value = args[i].to_param + yield key if value.nil? || value.empty? + params[key] = value + } params end - def missing_keys(args) - args.select{ |part, arg| arg.nil? || arg.empty? }.keys - end - - def raise_generation_error(args, missing_keys) - constraints = Hash[@route.requirements.merge(args).sort] + def raise_generation_error(args) + missing_keys = [] + params = parameterize_args(args) { |missing_key| + missing_keys << missing_key + } + constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}] message = "No route matches #{constraints.inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" @@ -264,9 +267,10 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - path_params.each { |param| - result[param] = inner_options.fetch(param) { args.shift } - } + path_params -= inner_options.keys + path_params.take(args.size).each do |param| + result[param] = args.shift + end end result.merge!(inner_options) @@ -299,15 +303,14 @@ module ActionDispatch end end - # :stopdoc: # strategy for building urls to send to the client PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } - # :startdoc: attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names - attr_accessor :default_url_options, :request_class + attr_accessor :default_url_options + attr_reader :env_key alias :routes :set @@ -315,22 +318,54 @@ module ActionDispatch { :new => 'new', :edit => 'edit' } end - def initialize(request_class = ActionDispatch::Request) + 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 + 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, :api_only + + DEFAULT_CONFIG = Config.new(nil, false) + + def initialize(config = DEFAULT_CONFIG) self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names self.default_url_options = {} - self.request_class = request_class + @config = config @append = [] @prepend = [] @disable_clear_and_finalize = false @finalized = false + @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end + def relative_url_root + @config.relative_url_root + end + + def api_only? + @config.api_only + end + + def request_class + ActionDispatch::Request + end + def draw(&block) clear! unless @disable_clear_and_finalize eval_block(block) @@ -378,7 +413,7 @@ module ActionDispatch Routing::RouteSet::Dispatcher.new(defaults) end - module MountedHelpers #:nodoc: + module MountedHelpers extend ActiveSupport::Concern include UrlFor end @@ -395,9 +430,11 @@ module ActionDispatch return if MountedHelpers.method_defined?(name) routes = self + helpers = routes.url_helpers + MountedHelpers.class_eval do define_method "_#{name}" do - RoutesProxy.new(routes, _routes_context) + RoutesProxy.new(routes, _routes_context, helpers) end end @@ -419,7 +456,14 @@ module ActionDispatch # Rails.application.routes.url_helpers.url_for(args) @_routes = routes class << self - delegate :url_for, :optimize_routes_generation?, to: '@_routes' + def url_for(options) + @_routes.url_for(options) + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + attr_reader :_routes def url_options; {}; end end @@ -477,10 +521,11 @@ module ActionDispatch path = conditions.delete :path_info ast = conditions.delete :parsed_path_info + required_defaults = conditions.delete :required_defaults path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map(&:to_sym)) + conditions = build_conditions(conditions) - route = @set.add_route(app, path, conditions, defaults, name) + route = @set.add_route(app, path, conditions, required_defaults, defaults, name) named_routes[name] = route if name route end @@ -516,7 +561,7 @@ module ActionDispatch end private :build_path - def build_conditions(current_conditions, path_values) + def build_conditions(current_conditions) conditions = current_conditions.dup # Rack-Mount requires that :request_method be a regular expression. @@ -529,13 +574,12 @@ module ActionDispatch end conditions.keep_if do |k, _| - k == :action || k == :controller || k == :required_defaults || - @request_class.public_method_defined?(k) || path_values.include?(k) + request_class.public_method_defined?(k) end end private :build_conditions - class Generator #:nodoc: + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value @@ -684,10 +728,10 @@ module ActionDispatch end def find_script_name(options) - options.delete(:script_name) || '' + options.delete(:script_name) || relative_url_root || '' end - def path_for(options, route_name = nil) # :nodoc: + def path_for(options, route_name = nil) url_for(options, route_name, PATH) end @@ -773,5 +817,6 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end end + # :startdoc: end end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index e2393d3799..040ea04046 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -8,8 +8,9 @@ module ActionDispatch attr_accessor :scope, :routes alias :_routes :routes - def initialize(routes, scope) + def initialize(routes, scope, helpers) @routes, @scope = routes, scope + @helpers = helpers end def url_options @@ -19,16 +20,16 @@ module ActionDispatch end def respond_to?(method, include_private = false) - super || routes.url_helpers.respond_to?(method) + super || @helpers.respond_to?(method) end def method_missing(method, *args) - if routes.url_helpers.respond_to?(method) + if @helpers.respond_to?(method) self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args) options = args.extract_options! args << url_options.merge((options || {}).symbolize_keys) - routes.url_helpers.#{method}(*args) + @helpers.#{method}(*args) end RUBY send(method, *args) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index dca86858cc..eb554ec383 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -184,12 +184,6 @@ module ActionDispatch def _routes_context self end - - private - - def _generate_paths_by_default - true - end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index f325c35b57..21b3b89d22 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type =~ /xml$/ + @html_document ||= if @response.content_type === Mime::XML Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 28dc88d317..c94eea9134 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -38,18 +38,24 @@ module ActionDispatch # # Test a custom route # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1') def assert_recognizes(expected_options, path, extras={}, msg=nil) - request = recognized_request_for(path, extras, msg) + if path.is_a?(Hash) && path[:method].to_s == "all" + [:get, :post, :put, :delete].each do |method| + assert_recognizes(expected_options, path.merge(method: method), extras, msg) + end + else + request = recognized_request_for(path, extras, msg) - expected_options = expected_options.clone + expected_options = expected_options.clone - expected_options.stringify_keys! + expected_options.stringify_keys! - msg = message(msg, "") { - sprintf("The recognized options <%s> did not match <%s>, difference:", - request.path_parameters, expected_options) - } + msg = message(msg, "") { + sprintf("The recognized options <%s> did not match <%s>, difference:", + request.path_parameters, expected_options) + } - assert_equal(expected_options, request.path_parameters, msg) + assert_equal(expected_options, request.path_parameters, msg) + end end # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. @@ -144,7 +150,7 @@ module ActionDispatch old_controller, @controller = @controller, @controller.clone _routes = @routes - @controller.singleton_class.send(:include, _routes.url_helpers) + @controller.singleton_class.include(_routes.url_helpers) @controller.view_context_class = Class.new(@controller.view_context_class) do include _routes.url_helpers end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index f0e2c5becc..dc664d5540 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,6 +2,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' +require 'active_support/core_ext/string/strip' require 'rack/test' require 'minitest' @@ -12,12 +13,14 @@ module ActionDispatch # # - +path+: The URI (as a String) on which you want to perform a GET # request. - # - +parameters+: The HTTP parameters that you want to pass. This may + # - +params+: The HTTP parameters that you want to pass. This may # be +nil+, # a Hash, or a String that is appropriately encoded # (<tt>application/x-www-form-urlencoded</tt> or # <tt>multipart/form-data</tt>). - # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be + # - +headers+: Additional headers to pass, as a Hash. The headers will be + # merged into the Rack env hash. + # - +env+: Additional env to pass, as a Hash. The headers will be # merged into the Rack env hash. # # This method returns a Response object, which one can use to @@ -28,38 +31,43 @@ module ActionDispatch # # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - def get(path, parameters = nil, headers_or_env = nil) - process :get, path, parameters, headers_or_env + # + # Example: + # + # get '/feed', params: { since: 201501011400 } + # post '/profile', headers: { "X-Test-Header" => "testvalue" } + def get(path, *args) + process_with_kwargs(:get, path, *args) end # Performs a POST request with the given parameters. See +#get+ for more # details. - def post(path, parameters = nil, headers_or_env = nil) - process :post, path, parameters, headers_or_env + def post(path, *args) + process_with_kwargs(:post, path, *args) end # Performs a PATCH request with the given parameters. See +#get+ for more # details. - def patch(path, parameters = nil, headers_or_env = nil) - process :patch, path, parameters, headers_or_env + def patch(path, *args) + process_with_kwargs(:patch, path, *args) end # Performs a PUT request with the given parameters. See +#get+ for more # details. - def put(path, parameters = nil, headers_or_env = nil) - process :put, path, parameters, headers_or_env + def put(path, *args) + process_with_kwargs(:put, path, *args) end # Performs a DELETE request with the given parameters. See +#get+ for # more details. - def delete(path, parameters = nil, headers_or_env = nil) - process :delete, path, parameters, headers_or_env + def delete(path, *args) + process_with_kwargs(:delete, path, *args) end # Performs a HEAD request with the given parameters. See +#get+ for more # details. - def head(path, parameters = nil, headers_or_env = nil) - process :head, path, parameters, headers_or_env + def head(path, *args) + process_with_kwargs(:head, path, *args) end # Performs an XMLHttpRequest request with the given parameters, mirroring @@ -68,11 +76,29 @@ module ActionDispatch # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart # string; the headers are a hash. - def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil) - headers_or_env ||= {} - headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers_or_env) + # + # Example: + # + # xhr :get, '/feed', params: { since: 201501011400 } + def xml_http_request(request_method, path, *args) + if kwarg_request?(args) + params, headers, env = args.first.values_at(:params, :headers, :env) + else + params = args[0] + headers = args[1] + env = {} + + if params.present? || headers.present? + non_kwarg_request_warning + end + end + + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + xhr and xml_http_request methods are deprecated in favor of + `get "/posts", xhr: true` and `post "/posts/1", xhr: true` + MSG + + process(request_method, path, params: params, headers: headers, xhr: true) end alias xhr :xml_http_request @@ -89,40 +115,52 @@ module ActionDispatch # redirect. Note that the redirects are followed until the response is # not a redirect--this means you may run into an infinite loop if your # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil) - process(http_method, path, parameters, headers_or_env) + # + # Example: + # + # request_via_redirect :post, '/welcome', + # params: { ref_id: 14 }, + # headers: { "X-Test-Header" => "testvalue" } + def request_via_redirect(http_method, path, *args) + process_with_kwargs(http_method, path, *args) + follow_redirect! while redirect? status end # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers_or_env = nil) - request_via_redirect(:get, path, parameters, headers_or_env) + def get_via_redirect(path, *args) + ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + request_via_redirect(:get, path, *args) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers_or_env = nil) - request_via_redirect(:post, path, parameters, headers_or_env) + def post_via_redirect(path, *args) + ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + request_via_redirect(:post, path, *args) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def patch_via_redirect(path, parameters = nil, headers_or_env = nil) - request_via_redirect(:patch, path, parameters, headers_or_env) + def patch_via_redirect(path, *args) + ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + request_via_redirect(:patch, path, *args) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers_or_env = nil) - request_via_redirect(:put, path, parameters, headers_or_env) + def put_via_redirect(path, *args) + ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + request_via_redirect(:put, path, *args) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers_or_env = nil) - request_via_redirect(:delete, path, parameters, headers_or_env) + def delete_via_redirect(path, *args) + ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + request_via_redirect(:delete, path, *args) end end @@ -185,15 +223,6 @@ module ActionDispatch super() @app = app - # If the app is a Rails app, make url_helpers available on the session - # This makes app.url_for and app.foo_path available in the console - if app.respond_to?(:routes) - singleton_class.class_eval do - include app.routes.url_helpers - include app.routes.mounted_helpers - end - end - reset! end @@ -261,8 +290,38 @@ module ActionDispatch @_mock_session ||= Rack::MockSession.new(@app, host) end + def process_with_kwargs(http_method, path, *args) + if kwarg_request?(args) + process(http_method, path, *args) + else + non_kwarg_request_warning if args.any? + process(http_method, path, { params: args[0], headers: args[1] }) + end + end + + REQUEST_KWARGS = %i(params headers env xhr) + def kwarg_request?(args) + args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } + end + + def non_kwarg_request_warning + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + ActionDispatch::IntegrationTest HTTP request methods will accept only + the following keyword arguments in future Rails versions: + #{REQUEST_KWARGS.join(', ')} + + Examples: + + get '/profile', + params: { id: 1 }, + headers: { 'X-Extra-Header' => '123' }, + env: { 'action_dispatch.custom' => 'custom' }, + xhr: true + MSG + end + # Performs the actual request. - def process(method, path, parameters = nil, headers_or_env = nil) + def process(method, path, params: nil, headers: nil, env: nil, xhr: false) if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -272,9 +331,9 @@ module ActionDispatch hostname, port = host.split(':') - env = { + request_env = { :method => method, - :params => parameters, + :params => params, "SERVER_NAME" => hostname, "SERVER_PORT" => port || (https? ? "443" : "80"), @@ -287,25 +346,37 @@ module ActionDispatch "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => accept } - # this modifies the passed env directly - Http::Headers.new(env).merge!(headers_or_env || {}) + + if xhr + headers ||= {} + headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + end + + # this modifies the passed request_env directly + if headers.present? + Http::Headers.new(request_env).merge!(headers) + end + if env.present? + Http::Headers.new(request_env).merge!(env) + end session = Rack::Test::Session.new(_mock_session) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri - session.request(build_full_uri(path, env), env) + session.request(build_full_uri(path, request_env), request_env) @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) response = _mock_session.last_response - @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body) + @response = ActionDispatch::TestResponse.from_response(response) @html_document = nil @url_options = nil @controller = session.last_request.env['action_controller.instance'] - return response.status + response.status end def build_full_uri(path, env) @@ -316,14 +387,36 @@ module ActionDispatch module Runner include ActionDispatch::Assertions - def app - @app ||= nil + APP_SESSIONS = {} + + attr_reader :app + + def before_setup + @app = nil + @integration_session = nil + super + end + + def integration_session + @integration_session ||= create_session(app) end # Reset the current session. This is useful for testing multiple sessions # in a single test case. def reset! - @integration_session = Integration::Session.new(app) + @integration_session = create_session(app) + end + + def create_session(app) + klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) { + # If the app is a Rails app, make url_helpers available on the session + # This makes app.url_for and app.foo_path available in the console + if app.respond_to?(:routes) + include app.routes.url_helpers + include app.routes.mounted_helpers + end + } + klass.new(app) end def remove! # :nodoc: @@ -333,12 +426,9 @@ module ActionDispatch %w(get post patch put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| - reset! unless integration_session - # 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 @@ -366,19 +456,16 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: - return unless integration_session - %w(controller response request).each do |var| - instance_variable_set("@#{var}", @integration_session.__send__(var)) - end + @controller = @integration_session.controller + @response = @integration_session.response + @request = @integration_session.request end def default_url_options - reset! unless integration_session integration_session.default_url_options end def default_url_options=(options) - reset! unless integration_session integration_session.default_url_options = options end @@ -388,7 +475,6 @@ module ActionDispatch # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) - reset! unless integration_session if integration_session.respond_to?(sym) integration_session.__send__(sym, *args, &block).tap do copy_session_variables! @@ -397,11 +483,6 @@ module ActionDispatch super end end - - private - def integration_session - @integration_session ||= nil - end end end @@ -424,8 +505,8 @@ module ActionDispatch # assert_equal 200, status # # # post the login and follow through to the home page - # post "/login", username: people(:jamis).username, - # password: people(:jamis).password + # post "/login", params: { username: people(:jamis).username, + # password: people(:jamis).password } # follow_redirect! # assert_equal 200, status # assert_equal "/home", path @@ -464,7 +545,7 @@ module ActionDispatch # end # # def speak(room, message) - # xml_http_request "/say/#{room.id}", message: message + # post "/say/#{room.id}", xhr: true, params: { message: message } # assert(...) # ... # end @@ -474,8 +555,8 @@ module ActionDispatch # open_session do |sess| # sess.extend(CustomAssertions) # who = people(who) - # sess.post "/login", username: who.username, - # password: who.password + # sess.post "/login", params: { username: who.username, + # password: who.password } # assert(...) # end # end @@ -494,14 +575,15 @@ module ActionDispatch # get "/login" # assert_response :success # - # post_via_redirect "/login", username: users(:david).username, password: users(:david).password + # post "/login", params: { username: users(:david).username, password: users(:david).password } + # follow_redirect! # assert_equal '/welcome', path # assert_equal 'Welcome david!', flash[:notice] # # https!(false) # get "/articles/all" # assert_response :success - # assert assigns(:articles) + # assert_select 'h1', 'Articles' # end # end # @@ -540,7 +622,7 @@ module ActionDispatch # def browses_site # get "/products/all" # assert_response :success - # assert assigns(:products) + # assert_select 'h1', 'Products' # end # end # @@ -549,7 +631,7 @@ module ActionDispatch # sess.extend(CustomDsl) # u = users(user) # sess.https! - # sess.post "/login", username: u.username, password: u.password + # sess.post "/login", params: { username: u.username, password: u.password } # assert_equal '/welcome', sess.path # sess.https!(false) # end @@ -578,7 +660,6 @@ module ActionDispatch end def url_options - reset! unless integration_session integration_session.url_options end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 630e6a9b78..415ef80cd2 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 diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 82039e72e7..a9b88ac5fd 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -7,11 +7,7 @@ module ActionDispatch # See Response for more information on controller response objects. class TestResponse < Response def self.from_response(response) - new.tap do |resp| - resp.status = response.status - resp.headers = response.headers - resp.body = response.body - end + new response.status, response.headers, response.body, default_headers: nil end # Was the response successful? diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 8cba049485..07571602e4 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -267,9 +267,11 @@ module AbstractController end class AliasedCallbacks < ControllerWithCallbacks - before_filter :first - after_filter :second - around_filter :aroundz + ActiveSupport::Deprecation.silence do + before_filter :first + after_filter :second + around_filter :aroundz + end def first @text = "Hello world" diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 4fdc480b43..8289252dfc 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -9,6 +9,22 @@ module AbstractController class TranslationControllerTest < ActiveSupport::TestCase def setup @controller = TranslationController.new + I18n.backend.store_translations(:en, { + one: { + two: 'bar', + }, + abstract_controller: { + testing: { + translation: { + index: { + foo: 'bar', + }, + no_action: 'no_action_tr', + }, + }, + }, + }) + @controller.stubs(action_name: :index) end def test_action_controller_base_responds_to_translate @@ -28,16 +44,19 @@ module AbstractController end def test_lazy_lookup - expected = 'bar' - @controller.stubs(action_name: :index) - I18n.stubs(:translate).with('abstract_controller.testing.translation.index.foo').returns(expected) - assert_equal expected, @controller.t('.foo') + assert_equal 'bar', @controller.t('.foo') + end + + def test_lazy_lookup_with_symbol + assert_equal 'bar', @controller.t(:'.foo') + end + + def test_lazy_lookup_fallback + assert_equal 'no_action_tr', @controller.t(:'.no_action') end def test_default_translation - key, expected = 'one.two', 'bar' - I18n.stubs(:translate).with(key).returns(expected) - assert_equal expected, @controller.t(key) + assert_equal 'bar', @controller.t('one.two') end def test_localize diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 2d69ceed3a..cc610b6d75 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -14,7 +14,11 @@ silence_warnings do end require 'drb' -require 'drb/unix' +begin + require 'drb/unix' +rescue LoadError + puts "'drb/unix' is not available" +end require 'tempfile' PROCESS_COUNT = (ENV['N'] || 4).to_i @@ -54,23 +58,8 @@ I18n.enforce_available_locales = false # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} -ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') -FIXTURES = Pathname.new(FIXTURE_LOAD_PATH) - -module RackTestUtils - def body_to_string(body) - if body.respond_to?(:each) - str = "" - body.each {|s| str << s } - str - else - body - end - end - extend self -end SharedTestRoutes = ActionDispatch::Routing::RouteSet.new @@ -110,7 +99,7 @@ end module ActiveSupport class TestCase include ActionDispatch::DrawOnce - if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 + if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 parallelize_me! end end @@ -129,22 +118,6 @@ class RoutedRackApp end end -class BasicController - attr_accessor :request - - def config - @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| - # VIEW TODO: View tests should not require a controller - public_dir = File.expand_path("../fixtures/public", __FILE__) - config.assets_dir = public_dir - config.javascripts_dir = "#{public_dir}/javascripts" - config.stylesheets_dir = "#{public_dir}/stylesheets" - config.assets = ActiveSupport::InheritableOptions.new({ :prefix => "assets" }) - config - end - end -end - class ActionDispatch::IntegrationTest < ActiveSupport::TestCase include ActionDispatch::SharedRoutes @@ -259,6 +232,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) @@ -407,7 +384,7 @@ module RoutingTestHelpers end class ResourcesController < ActionController::Base - def index() render :nothing => true end + def index() head :ok end alias_method :show, :index end @@ -510,12 +487,7 @@ class ForkingExecutor end end -if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 +if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 # Use N processes (N defaults to 4) Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT) end - -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index d0dfbfbc74..5b85e83045 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 @@ -73,16 +68,6 @@ class ActionPackAssertionsController < ActionController::Base 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" - end - def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" @@ -123,7 +108,7 @@ end module Admin class InnerModuleController < ActionController::Base def index - render :nothing => true + head :ok end def redirect_to_index @@ -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 @@ -378,7 +355,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end def test_render_based_on_parameters - process :render_based_on_parameters, "GET", "name" => "David" + process :render_based_on_parameters, + method: "GET", + params: { name: "David" } assert_equal "Mr. David", @response.body end @@ -453,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..d1eb27bf24 --- /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 text: "Hi!" + end + end + + def two + render text: "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..e40a39d829 --- /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) + 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 950788743e..3240185414 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -1,31 +1,11 @@ require 'abstract_unit' require 'active_support/logger' require 'controller/fake_models' -require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late # Provide some controller to run the tests on. module Submodule class ContainedEmptyController < ActionController::Base end - - class ContainedNonEmptyController < ActionController::Base - def public_action - render :nothing => true - end - - hide_action :hidden_action - def hidden_action - raise "Noooo!" - end - - def another_hidden_action - end - hide_action :another_hidden_action - end - - class SubclassedController < ContainedNonEmptyController - hide_action :public_action # Hiding it here should not affect the superclass. - end end class EmptyController < ActionController::Base @@ -33,11 +13,7 @@ end class NonEmptyController < ActionController::Base def public_action - render :nothing => true - end - - hide_action :hidden_action - def hidden_action + head :ok end end @@ -51,6 +27,16 @@ class DefaultUrlOptionsController < ActionController::Base end end +class OptionalDefaultUrlOptionsController < ActionController::Base + def show + head :ok + end + + def default_url_options + { format: 'atom', id: 'default-id' } + end +end + class UrlOptionsController < ActionController::Base def from_view render :inline => "<%= #{params[:route]} %>" @@ -108,10 +94,7 @@ class ControllerInstanceTests < ActiveSupport::TestCase def setup @empty = EmptyController.new @contained = Submodule::ContainedEmptyController.new - @empty_controllers = [@empty, @contained, Submodule::SubclassedController.new] - - @non_empty_controllers = [NonEmptyController.new, - Submodule::ContainedNonEmptyController.new] + @empty_controllers = [@empty, @contained] end def test_performed? @@ -124,10 +107,6 @@ class ControllerInstanceTests < ActiveSupport::TestCase @empty_controllers.each do |c| assert_equal Set.new, c.class.action_methods, "#{c.controller_path} should be empty!" end - - @non_empty_controllers.each do |c| - assert_equal Set.new(%w(public_action)), c.class.action_methods, "#{c.controller_path} should not be empty!" - end end def test_temporary_anonymous_controllers @@ -161,12 +140,6 @@ class PerformActionTest < ActionController::TestCase assert_equal "The action 'non_existent' could not be found for EmptyController", exception.message end - def test_get_on_hidden_should_fail - use_controller NonEmptyController - assert_raise(AbstractController::ActionNotFound) { get :hidden_action } - assert_raise(AbstractController::ActionNotFound) { get :another_hidden_action } - end - def test_action_missing_should_work use_controller ActionMissingController get :arbitrary_action @@ -205,7 +178,7 @@ class UrlOptionsTest < ActionController::TestCase get ':controller/:action' end - get :from_view, :route => "from_view_url" + get :from_view, params: { route: "from_view_url" } assert_equal 'http://www.override.com/from_view', @response.body assert_equal 'http://www.override.com/from_view', @controller.send(:from_view_url) @@ -239,7 +212,7 @@ class DefaultUrlOptionsTest < ActionController::TestCase get ':controller/:action' end - get :from_view, :route => "from_view_url" + get :from_view, params: { route: "from_view_url" } assert_equal 'http://www.override.com/from_view?locale=en', @response.body assert_equal 'http://www.override.com/from_view?locale=en', @controller.send(:from_view_url) @@ -256,7 +229,7 @@ class DefaultUrlOptionsTest < ActionController::TestCase get ':controller/:action' end - get :from_view, :route => "description_path(1)" + get :from_view, params: { route: "description_path(1)" } assert_equal '/en/descriptions/1', @response.body assert_equal '/en/descriptions', @controller.send(:descriptions_path) @@ -271,7 +244,18 @@ class DefaultUrlOptionsTest < ActionController::TestCase assert_equal '/en/descriptions/1.xml', @controller.send(:description_path, 1, :format => "xml") end end +end +class OptionalDefaultUrlOptionsControllerTest < ActionController::TestCase + def test_default_url_options_override_missing_positional_arguments + with_routing do |set| + set.draw do + get "/things/:id(.:format)" => 'things#show', :as => :thing + end + assert_equal '/things/1.atom', thing_path('1') + assert_equal '/things/default-id.atom', thing_path + end + end end class EmptyUrlOptionsTest < ActionController::TestCase diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index c0e6a2ebd1..de697858c3 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -1,5 +1,6 @@ require 'fileutils' require 'abstract_unit' +require 'lib/controller/fake_models' CACHE_DIR = 'test_cache' # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed @@ -210,7 +211,7 @@ CACHED end def test_skipping_fragment_cache_digesting - get :fragment_cached_without_digest, :format => "html" + get :fragment_cached_without_digest, format: "html" assert_response :success expected_body = "<body>\n<p>ERB</p>\n</body>\n" @@ -244,7 +245,7 @@ CACHED end def test_html_formatted_fragment_caching - get :formatted_fragment_cached, :format => "html" + get :formatted_fragment_cached, format: "html" assert_response :success expected_body = "<body>\n<p>ERB</p>\n</body>\n" @@ -255,7 +256,7 @@ CACHED end def test_xml_formatted_fragment_caching - get :formatted_fragment_cached, :format => "xml" + get :formatted_fragment_cached, format: "xml" assert_response :success expected_body = "<body>\n <p>Builder</p>\n</body>\n" @@ -269,7 +270,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" assert_response :success expected_body = "<body>\n<p>PHONE</p>\n</body>\n" @@ -349,3 +350,64 @@ class ViewCacheDependencyTest < ActionController::TestCase assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies end end + +class CollectionCacheController < ActionController::Base + attr_accessor :partial_rendered_times + + def index + @customers = [Customer.new('david', params[:id] || 1)] + end + + def index_ordered + @customers = [Customer.new('david', 1), Customer.new('david', 2), Customer.new('david', 3)] + render 'index' + end + + def index_explicit_render + @customers = [Customer.new('david', 1)] + render partial: 'customers/customer', collection: @customers + end + + def index_with_comment + @customers = [Customer.new('david', 1)] + render partial: 'customers/commented_customer', collection: @customers, as: :customer + end +end + +class AutomaticCollectionCacheTest < ActionController::TestCase + def setup + super + @controller = CollectionCacheController.new + @controller.perform_caching = true + @controller.partial_rendered_times = 0 + end + + def test_collection_fetches_cached_views + get :index + assert_equal 1, @controller.partial_rendered_times + + get :index + assert_equal 1, @controller.partial_rendered_times + end + + def test_preserves_order_when_reading_from_cache_plus_rendering + get :index, params: { id: 2 } + get :index_ordered + + assert_select ':root', "david, 1\n david, 2\n david, 3" + end + + def test_explicit_render_call_with_options + get :index_explicit_render + + assert_select ':root', "david, 1" + end + + def test_caching_works_with_beginning_comment + get :index_with_comment + assert_equal 1, @controller.partial_rendered_times + + get :index_with_comment + assert_equal 1, @controller.partial_rendered_times + end +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 656fd0431e..230f40d7ad 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 @@ -1,6 +1,5 @@ require 'abstract_unit' - class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base before_action { I18n.locale = params[:locale] } @@ -23,7 +22,7 @@ class ControllerWithBeforeActionAndDefaultUrlOptionsTest < ActionController::Tes # This test has its roots in issue #1872 test "should redirect with correct locale :de" do - get :redirect, :locale => "de" + get :redirect, params: { locale: "de" } assert_redirected_to "/controller_with_before_action_and_default_url_options/target?locale=de" end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 2e08a6af9f..9b0487841f 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -13,20 +13,9 @@ 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 - class TestController < ActionController::Base before_action :ensure_login after_action :clean_up @@ -225,6 +214,30 @@ class FilterTest < ActionController::TestCase skip_before_action :clean_up_tmp, if: -> { true } end + class SkipFilterUsingOnlyAndIf < ConditionalFilterController + before_action :clean_up_tmp + before_action :ensure_login + + skip_before_action :ensure_login, only: :login, if: -> { false } + skip_before_action :clean_up_tmp, only: :login, if: -> { true } + + def login + render text: 'ok' + end + end + + class SkipFilterUsingIfAndExcept < ConditionalFilterController + before_action :clean_up_tmp + before_action :ensure_login + + skip_before_action :ensure_login, if: -> { false }, except: :login + skip_before_action :clean_up_tmp, if: -> { true }, except: :login + + def login + render text: 'ok' + end + end + class ClassController < ConditionalFilterController before_action ConditionalClassFilter end @@ -537,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 @@ -554,130 +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_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), @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 @@ -690,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 @@ -742,26 +766,26 @@ class FilterTest < ActionController::TestCase 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") @@ -771,23 +795,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 @@ -802,13 +826,13 @@ class FilterTest < ActionController::TestCase 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 @@ -933,8 +957,15 @@ class ControllerWithAllTypesOfFilters < PostsController end class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters - skip_action_callback :around_again - skip_action_callback :after + skip_around_action :around_again + skip_after_action :after +end + +class SkipFilterUsingSkipActionCallback < ControllerWithAllTypesOfFilters + ActiveSupport::Deprecation.silence do + skip_action_callback :around_again + skip_action_callback :after + end end class YieldingAroundFiltersTest < ActionController::TestCase @@ -970,8 +1001,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 @@ -992,12 +1023,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 @@ -1021,6 +1052,27 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert_equal 3, controller.instance_variable_get(:@try) end + def test_skipping_with_skip_action_callback + test_process(SkipFilterUsingSkipActionCallback,'no_raise') + assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') + end + + def test_deprecated_skip_action_callback + assert_deprecated do + Class.new(PostsController) do + skip_action_callback :clean_up + end + end + end + + def test_deprecated_skip_filter + assert_deprecated do + Class.new(PostsController) do + skip_filter :clean_up + end + end + end + protected def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index d979b561f2..081288ef21 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -57,33 +57,36 @@ module ActionDispatch def test_to_session_value @hash['foo'] = 'bar' - assert_equal({'flashes' => {'foo' => 'bar'}, 'discard' => []}, @hash.to_session_value) - - @hash.discard('foo') - assert_equal({'flashes' => {'foo' => 'bar'}, 'discard' => %w[foo]}, @hash.to_session_value) + assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value) @hash.now['qux'] = 1 - assert_equal({'flashes' => {'foo' => 'bar', 'qux' => 1}, 'discard' => %w[foo qux]}, @hash.to_session_value) + assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value) + + @hash.discard('foo') + assert_equal(nil, @hash.to_session_value) @hash.sweep assert_equal(nil, @hash.to_session_value) end def test_from_session_value - rails_3_2_cookie = 'BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsAOgxAY2xvc2VkRjoNQGZsYXNoZXN7BkkiDG1lc3NhZ2UGOwBGSSIKSGVsbG8GOwBGOglAbm93MA==' + # {"session_id"=>"f8e1b8152ba7609c28bbb17ec9263ba7", "flash"=>#<ActionDispatch::Flash::FlashHash:0x00000000000000 @used=#<Set: {"farewell"}>, @closed=false, @flashes={"greeting"=>"Hello", "farewell"=>"Goodbye"}, @now=nil>} + rails_3_2_cookie = 'BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsGSSINZmFyZXdlbGwGOwBUVDoMQGNsb3NlZEY6DUBmbGFzaGVzewdJIg1ncmVldGluZwY7AFRJIgpIZWxsbwY7AFRJIg1mYXJld2VsbAY7AFRJIgxHb29kYnllBjsAVDoJQG5vdzA=' session = Marshal.load(Base64.decode64(rails_3_2_cookie)) hash = Flash::FlashHash.from_session_value(session['flash']) - assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value) + assert_equal({'greeting' => 'Hello'}, hash.to_hash) + assert_equal(nil, hash.to_session_value) end def test_from_session_value_on_json_serializer - decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }" + decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[\"farewell\"], \"flashes\":{\"greeting\":\"Hello\",\"farewell\":\"Goodbye\"}} }" session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data) hash = Flash::FlashHash.from_session_value(session['flash']) - assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value) - assert_equal "hey you", hash[:message] - assert_equal "hey you", hash["message"] + assert_equal({'greeting' => 'Hello'}, hash.to_hash) + assert_equal(nil, hash.to_session_value) + assert_equal "Hello", hash[:greeting] + assert_equal "Hello", hash["greeting"] end def test_empty? diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 3720a920d0..64543f0659 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -57,7 +57,7 @@ class FlashTest < ActionController::TestCase def std_action @flash_copy = {}.update(flash) - render :nothing => true + head :ok end def filter_halting_action @@ -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 @@ -288,16 +289,16 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest def test_setting_flash_does_not_raise_in_following_requests with_test_route_set do env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/set_flash', nil, env - get '/set_flash', nil, env + get '/set_flash', env: env + get '/set_flash', env: env end end def test_setting_flash_now_does_not_raise_in_following_requests with_test_route_set do env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/set_flash_now', nil, env - get '/set_flash_now', nil, env + get '/set_flash_now', env: env + get '/set_flash_now', env: env end end @@ -312,9 +313,11 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest private # Overwrite get to send SessionSecret in env hash - def get(path, parameters = nil, env = {}) - env["action_dispatch.key_generator"] ||= Generator - super + def get(path, *args) + args[0] ||= {} + args[0][:env] ||= {} + args[0][:env]["action_dispatch.key_generator"] ||= Generator + super(path, *args) end def with_test_route_set diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 00d4612ac9..72ae30eb39 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -100,7 +100,7 @@ class ForceSSLControllerLevelTest < ActionController::TestCase end def test_banana_redirects_to_https_with_extra_params - get :banana, :token => "secret" + get :banana, params: { token: "secret" } assert_response 301 assert_equal "https://test.host/force_ssl_controller_level/banana?token=secret", redirect_to_url 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 @@ -273,7 +273,7 @@ class ForceSSLFormatTest < ActionController::TestCase get '/foo', :to => 'force_ssl_controller_level#banana' end - get :banana, :format => :json + get :banana, format: :json assert_response 301 assert_equal 'https://test.host/foo.json', redirect_to_url end @@ -294,7 +294,7 @@ class ForceSSLOptionalSegmentsTest < ActionController::TestCase end @request.env['PATH_INFO'] = '/en/foo' - get :banana, :locale => 'en' + get :banana, params: { locale: 'en' } assert_equal 'en', @controller.params[:locale] assert_response 301 assert_equal 'https://test.host/en/foo', redirect_to_url @@ -315,7 +315,7 @@ class RedirectToSSLTest < ActionController::TestCase assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url end - def test_banana_does_not_redirect_if_already_https + def test_cheeseburgers_does_not_redirect_if_already_https request.env['HTTPS'] = 'on' get :cheeseburger assert_response 200 diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb new file mode 100644 index 0000000000..99eeaf9ab6 --- /dev/null +++ b/actionpack/test/controller/form_builder_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' + +class FormBuilderController < ActionController::Base + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + default_form_builder SpecializedFormBuilder +end + +class ControllerFormBuilderTest < ActiveSupport::TestCase + setup do + @controller = FormBuilderController.new + end + + def test_default_form_builder_assigned + assert_equal FormBuilderController::SpecializedFormBuilder, @controller.default_form_builder + end +end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 936b8c2450..e263ed341f 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -29,7 +29,7 @@ module ImpressiveLibrary def useful_function() end end -ActionController::Base.send :include, ImpressiveLibrary +ActionController::Base.include(ImpressiveLibrary) class JustMeController < ActionController::Base clear_helpers @@ -223,10 +223,10 @@ class HelperTest < ActiveSupport::TestCase # fun/pdf_helper.rb assert methods.include?(:foobar) end - + def test_helper_proxy_config AllHelpersController.config.my_var = 'smth' - + assert_equal 'smth', AllHelpersController.helpers.config.my_var end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 9052fc6962..adaf19c2dc 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -13,7 +13,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase end def display - render :text => 'Definitely Maybe' + render :text => 'Definitely Maybe' if @logged_in end def show @@ -36,7 +36,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase if authenticate_with_http_basic { |username, password| username == 'pretty' && password == 'please' } @logged_in = true else - request_http_basic_authentication("SuperSecret") + request_http_basic_authentication("SuperSecret", "Authentication Failed\n") end end @@ -83,6 +83,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_response :unauthorized assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials" end + + test "unsuccessful authentication with #{header.downcase} and no credentials" do + get :show + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and no credentials" + end end def test_encode_credentials_has_no_newline @@ -97,7 +104,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -106,7 +113,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -115,7 +122,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..57964769a7 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -14,7 +14,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase end def display - render :text => 'Definitely Maybe' + render :text => '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 a758df2ec6..802c17b6bf 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -30,7 +30,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' } @logged_in = true else - request_http_token_authentication("SuperSecret") + request_http_token_authentication("SuperSecret", "Authentication Failed\n") end end @@ -80,18 +80,25 @@ 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 assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -100,7 +107,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 5535c7ae78..a6460168cb 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -32,158 +32,275 @@ class SessionTest < ActiveSupport::TestCase def test_request_via_redirect_uses_given_method path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} - @session.expects(:process).with(:put, path, args, headers) + @session.expects(:process).with(:put, path, params: args, headers: headers) @session.stubs(:redirect?).returns(false) - @session.request_via_redirect(:put, path, args, headers) + @session.request_via_redirect(:put, path, params: args, headers: headers) + end + + def test_deprecated_request_via_redirect_uses_given_method + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } + @session.expects(:process).with(:put, path, params: args, headers: headers) + @session.stubs(:redirect?).returns(false) + assert_deprecated { @session.request_via_redirect(:put, path, args, headers) } end def test_request_via_redirect_follows_redirects path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} @session.stubs(:redirect?).returns(true, true, false) @session.expects(:follow_redirect!).times(2) - @session.request_via_redirect(:get, path, args, headers) + @session.request_via_redirect(:get, path, params: args, headers: headers) end def test_request_via_redirect_returns_status path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} @session.stubs(:redirect?).returns(false) @session.stubs(:status).returns(200) - assert_equal 200, @session.request_via_redirect(:get, path, args, headers) + assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) end - def test_get_via_redirect - path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + def test_deprecated_get_via_redirect + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:get, path, args, headers) - @session.get_via_redirect(path, args, headers) + + assert_deprecated do + @session.get_via_redirect(path, args, headers) + end end - def test_post_via_redirect - path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + def test_deprecated_post_via_redirect + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:post, path, args, headers) - @session.post_via_redirect(path, args, headers) + + assert_deprecated do + @session.post_via_redirect(path, args, headers) + end end - def test_patch_via_redirect - path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + def test_deprecated_patch_via_redirect + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:patch, path, args, headers) - @session.patch_via_redirect(path, args, headers) + + assert_deprecated do + @session.patch_via_redirect(path, args, headers) + end end - def test_put_via_redirect - path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + def test_deprecated_put_via_redirect + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:put, path, args, headers) - @session.put_via_redirect(path, args, headers) + + assert_deprecated do + @session.put_via_redirect(path, args, headers) + end end - def test_delete_via_redirect - path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + def test_deprecated_delete_via_redirect + path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:delete, path, args, headers) - @session.delete_via_redirect(path, args, headers) + + assert_deprecated do + @session.delete_via_redirect(path, args, headers) + end end def test_get - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:get,path,params,headers) - @session.get(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers) + @session.get(path, params: params, headers: headers) + end + + def test_get_with_env_and_headers + path = "/index"; params = "blah"; headers = { location: 'blah' }; env = { 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest' } + @session.expects(:process).with(:get, path, params: params, headers: headers, env: env) + @session.get(path, params: params, headers: headers, env: env) + end + + def test_deprecated_get + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers) + assert_deprecated { + @session.get(path, params, headers) + } end def test_post - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:post,path,params,headers) - @session.post(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers) + assert_deprecated { + @session.post(path, params, headers) + } + end + + def test_deprecated_post + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers) + @session.post(path, params: params, headers: headers) end def test_patch - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:patch,path,params,headers) - @session.patch(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers) + @session.patch(path, params: params, headers: headers) + end + + def test_deprecated_patch + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers) + assert_deprecated { + @session.patch(path, params, headers) + } end def test_put - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:put,path,params,headers) - @session.put(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers) + @session.put(path, params: params, headers: headers) + end + + def test_deprecated_put + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers) + assert_deprecated { + @session.put(path, params, headers) + } end def test_delete - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:delete,path,params,headers) - @session.delete(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers) + assert_deprecated { + @session.delete(path,params,headers) + } + end + + def test_deprecated_delete + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers) + @session.delete(path, params: params, headers: headers) end def test_head - path = "/index"; params = "blah"; headers = {:location => 'blah'} - @session.expects(:process).with(:head,path,params,headers) - @session.head(path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers) + @session.head(path, params: params, headers: headers) + end + + def deprecated_test_head + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers) + assert_deprecated { + @session.head(path, params, headers) + } end def test_xml_http_request_get - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:get,path,params,headers_after_xhr) - @session.xml_http_request(:get,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + @session.get(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_get + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + @session.get(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_get + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { + @session.xml_http_request(:get, path, params, headers) + } end def test_xml_http_request_post - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:post,path,params,headers_after_xhr) - @session.xml_http_request(:post,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + @session.post(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_post + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + @session.post(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_post + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) } end def test_xml_http_request_patch - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:patch,path,params,headers_after_xhr) - @session.xml_http_request(:patch,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + @session.patch(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_patch + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + @session.patch(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_patch + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) } end def test_xml_http_request_put - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:put,path,params,headers_after_xhr) - @session.xml_http_request(:put,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + @session.put(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_put + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + @session.put(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_put + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) } end def test_xml_http_request_delete - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:delete,path,params,headers_after_xhr) - @session.xml_http_request(:delete,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + @session.delete(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_delete + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) } + end + + def test_deprecated_args_xml_http_request_delete + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) } end def test_xml_http_request_head - path = "/index"; params = "blah"; headers = {:location => 'blah'} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:head,path,params,headers_after_xhr) - @session.xml_http_request(:head,path,params,headers) - end - - def test_xml_http_request_override_accept - path = "/index"; params = "blah"; headers = {:location => 'blah', "HTTP_ACCEPT" => "application/xml"} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - ) - @session.expects(:process).with(:post,path,params,headers_after_xhr) - @session.xml_http_request(:post,path,params,headers) + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + @session.head(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_xml_http_request_head + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) } + end + + def test_deprecated_args_xml_http_request_head + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + assert_deprecated { @session.xml_http_request(:head, path, params, headers) } end end @@ -198,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 @@ -246,6 +361,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest respond_to do |format| format.html { render :text => "OK", :status => 200 } format.js { render :text => "JS OK", :status => 200 } + format.xml { render :xml => "<root></root>", :status => 200 } end end @@ -279,6 +395,11 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def redirect redirect_to action_url('get') end + + def remove_header + response.headers.delete params[:header] + head :ok, 'c' => '3' + end end def test_get @@ -297,6 +418,22 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_get_xml + with_test_route_set do + get "/get", params: {}, headers: {"HTTP_ACCEPT" => "application/xml"} + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal({}, cookies.to_hash) + assert_equal "<root></root>", body + assert_equal "<root></root>", response.body + assert_instance_of Nokogiri::XML::Document, html_document + assert_equal 1, request_count + end + end + def test_post with_test_route_set do post '/post' @@ -383,7 +520,19 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def test_xml_http_request_get with_test_route_set do - xhr :get, '/get' + get '/get', xhr: true + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal "JS OK", response.body + end + end + + def test_deprecated_xml_http_request_get + with_test_route_set do + assert_deprecated { xhr :get, '/get' } assert_equal 200, status assert_equal "OK", status_message assert_response 200 @@ -395,7 +544,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def test_request_with_bad_format with_test_route_set do - xhr :get, '/get.php' + get '/get.php', xhr: true assert_equal 406, status assert_response 406 assert_response :not_acceptable @@ -418,7 +567,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def test_get_with_parameters with_test_route_set do - get '/get_with_params', :foo => "bar" + get '/get_with_params', params: { foo: "bar" } assert_equal '/get_with_params', request.env["PATH_INFO"] assert_equal '/get_with_params', request.path_info assert_equal 'foo=bar', request.env["QUERY_STRING"] @@ -506,7 +655,27 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_respect_removal_of_default_headers_by_a_controller_action + with_test_route_set do + with_default_headers 'a' => '1', 'b' => '2' do + get '/remove_header', params: { header: 'a' } + end + end + + assert_not_includes @response.headers, 'a', 'Response should not include default header removed by the controller action' + assert_includes @response.headers, 'b' + assert_includes @response.headers, 'c' + end + private + def with_default_headers(headers) + original = ActionDispatch::Response.default_headers + ActionDispatch::Response.default_headers = headers + yield + ensure + ActionDispatch::Response.default_headers = original + end + def with_test_route_set with_routing do |set| controller = ::IntegrationProcessTest::IntegrationController.clone @@ -521,7 +690,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest get 'get/:action', :to => controller, :as => :get_action end - self.singleton_class.send(:include, set.url_helpers) + self.singleton_class.include(set.url_helpers) yield end @@ -565,14 +734,22 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest end def test_pass_headers - get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" + get "/success", headers: {"Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com"} assert_equal "http://nohost.com", @request.env["HTTP_HOST"] assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] end + def test_pass_headers_and_env + get "/success", headers: { "X-Test-Header" => "value" }, env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" } + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + assert_equal "value", @request.env["HTTP_X_TEST_HEADER"] + end + def test_pass_env - get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" + get "/success", env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" } assert_equal "http://test.com", @request.env["HTTP_HOST"] assert_equal "http://test.com/", @request.env["HTTP_REFERER"] @@ -662,7 +839,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest test "process do not modify the env passed as argument" do env = { :SERVER_NAME => 'server', 'action_dispatch.custom' => 'custom' } old_env = env.dup - get '/foo', nil, env + get '/foo', env: env assert_equal old_env, env end end @@ -692,7 +869,7 @@ class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest end test "filters rack request form vars" do - post "/post", :username => 'cjolly', :password => 'secret' + post "/post", params: { username: 'cjolly', password: 'secret' } assert_equal 'cjolly', request.filtered_parameters['username'] assert_equal '[FILTERED]', request.filtered_parameters['password'] @@ -874,3 +1051,15 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest end end end + +# to ensure that session requirements in setup are persisted in the tests +class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest + setup do + cookies['user_name'] = 'david' + end + + def test_cookies_set_in_setup_are_persisted_through_the_session + get "/foo" + assert_equal({"user_name"=>"david"}, cookies.to_hash) + end +end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 7fd1276e98..0c65270ec1 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -276,6 +276,8 @@ module ActionController end def test_async_stream + rubinius_skip "https://github.com/rubinius/rubinius/issues/2934" + @controller.latch = ActiveSupport::Concurrency::Latch.new parts = ['hello', 'world'] diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index 2be947c648..3576015513 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -30,7 +30,7 @@ class LocalizedTemplatesTest < ActionController::TestCase def test_use_fallback_locales I18n.locale = :"de-AT" - I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + I18n.backend.class.include(I18n::Backend::Fallbacks) I18n.fallbacks[:"de-AT"] = [:de] get :hello_world diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 864c6ee130..ccbf336acf 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -19,7 +19,7 @@ module Another end def show - render :nothing => true + head :ok end def redirector @@ -140,7 +140,7 @@ class ACLogSubscriberTest < ActionController::TestCase end def test_process_action_with_parameters - get :show, :id => '10' + get :show, params: { id: '10' } wait assert_equal 3, logs.size @@ -148,8 +148,8 @@ class ACLogSubscriberTest < ActionController::TestCase end def test_multiple_process_with_parameters - get :show, :id => '10' - get :show, :id => '20' + get :show, params: { id: '10' } + get :show, params: { id: '20' } wait @@ -160,7 +160,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_wrapped_parameters @request.env['CONTENT_TYPE'] = 'application/json' - post :show, :id => '10', :name => 'jose' + post :show, params: { id: '10', name: 'jose' } wait assert_equal 3, logs.size @@ -170,7 +170,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_view_runtime get :show wait - assert_match(/\(Views: [\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 @@ -186,7 +186,9 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_filter_parameters @request.env["action_dispatch.parameter_filter"] = [:lifo, :amount] - get :show, :lifo => 'Pratik', :amount => '420', :step => '1' + get :show, params: { + lifo: 'Pratik', amount: '420', step: '1' + } wait params = logs[1] diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb index 811c507af2..e20c08da4e 100644 --- a/actionpack/test/controller/mime/accept_format_test.rb +++ b/actionpack/test/controller/mime/accept_format_test.rb @@ -11,7 +11,7 @@ end class StarStarMimeControllerTest < ActionController::TestCase def test_javascript_with_format @request.accept = "text/javascript" - get :index, :format => 'js' + get :index, format: 'js' assert_match "function addition(a,b){ return a+b; }", @response.body end @@ -86,7 +86,7 @@ class MimeControllerLayoutsTest < ActionController::TestCase end def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout - get :index, :format => :js + get :index, format: :js assert_equal "Hello Firefox", @response.body end end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 66d2fd7716..7aef8a50ce 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout @@ -310,17 +311,17 @@ class RespondToControllerTest < ActionController::TestCase def test_js_or_html @request.accept = "text/javascript, text/html" - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body @request.accept = "text/javascript, text/html" - xhr :get, :html_or_xml + get :html_or_xml, xhr: true assert_equal 'HTML', @response.body @request.accept = "text/javascript, text/html" assert_raises(ActionController::UnknownFormat) do - xhr :get, :just_xml + get :just_xml, xhr: true end end @@ -335,13 +336,13 @@ class RespondToControllerTest < ActionController::TestCase end def test_json_or_yaml - xhr :get, :json_or_yaml + get :json_or_yaml, xhr: true assert_equal 'JSON', @response.body - get :json_or_yaml, :format => 'json' + get :json_or_yaml, format: 'json' assert_equal 'JSON', @response.body - get :json_or_yaml, :format => 'yaml' + get :json_or_yaml, format: 'yaml' assert_equal 'YAML', @response.body { 'YAML' => %w(text/yaml), @@ -357,13 +358,13 @@ class RespondToControllerTest < ActionController::TestCase def test_js_or_anything @request.accept = "text/javascript, */*" - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body - xhr :get, :html_or_xml + get :html_or_xml, xhr: true assert_equal 'HTML', @response.body - xhr :get, :just_xml + get :just_xml, xhr: true assert_equal 'XML', @response.body end @@ -408,14 +409,14 @@ class RespondToControllerTest < ActionController::TestCase def test_with_atom_content_type @request.accept = "" @request.env["CONTENT_TYPE"] = "application/atom+xml" - xhr :get, :made_for_content_type + get :made_for_content_type, xhr: true assert_equal "ATOM", @response.body end def test_with_rss_content_type @request.accept = "" @request.env["CONTENT_TYPE"] = "application/rss+xml" - xhr :get, :made_for_content_type + get :made_for_content_type, xhr: true assert_equal "RSS", @response.body end @@ -474,7 +475,7 @@ class RespondToControllerTest < ActionController::TestCase end def test_handle_any_any_parameter_format - get :handle_any_any, {:format=>'html'} + get :handle_any_any, format: 'html' assert_equal 'HTML', @response.body end @@ -497,7 +498,7 @@ class RespondToControllerTest < ActionController::TestCase end def test_handle_any_any_unkown_format - get :handle_any_any, { format: 'php' } + get :handle_any_any, format: 'php' assert_equal 'Whatever you ask for, I got it', @response.body end @@ -525,18 +526,18 @@ class RespondToControllerTest < ActionController::TestCase end def test_xhr - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body end def test_custom_constant - get :custom_constant_handling, :format => "mobile" + get :custom_constant_handling, format: "mobile" assert_equal "text/x-mobile", @response.content_type assert_equal "Mobile", @response.body end def test_custom_constant_handling_without_block - get :custom_constant_handling_without_block, :format => "mobile" + get :custom_constant_handling_without_block, format: "mobile" assert_equal "text/x-mobile", @response.content_type assert_equal "Mobile", @response.body end @@ -545,13 +546,13 @@ class RespondToControllerTest < ActionController::TestCase get :html_xml_or_rss assert_equal "HTML", @response.body - get :html_xml_or_rss, :format => "html" + get :html_xml_or_rss, format: "html" assert_equal "HTML", @response.body - get :html_xml_or_rss, :format => "xml" + get :html_xml_or_rss, format: "xml" assert_equal "XML", @response.body - get :html_xml_or_rss, :format => "rss" + get :html_xml_or_rss, format: "rss" assert_equal "RSS", @response.body end @@ -559,12 +560,12 @@ class RespondToControllerTest < ActionController::TestCase get :forced_xml assert_equal "XML", @response.body - get :forced_xml, :format => "html" + get :forced_xml, format: "html" assert_equal "XML", @response.body end def test_extension_synonyms - get :html_xml_or_rss, :format => "xhtml" + get :html_xml_or_rss, format: "xhtml" assert_equal "HTML", @response.body end @@ -581,7 +582,7 @@ class RespondToControllerTest < ActionController::TestCase get :using_defaults assert_equal "using_defaults - #{[:html]}", @response.body - get :using_defaults, :format => "xml" + get :using_defaults, format: "xml" assert_equal "using_defaults - #{[:xml]}", @response.body end @@ -589,7 +590,7 @@ class RespondToControllerTest < ActionController::TestCase get :iphone_with_html_response_type assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body - get :iphone_with_html_response_type, :format => "iphone" + get :iphone_with_html_response_type, format: "iphone" assert_equal "text/html", @response.content_type assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body end @@ -603,24 +604,34 @@ class RespondToControllerTest < ActionController::TestCase def test_invalid_format assert_raises(ActionController::UnknownFormat) do - get :using_defaults, :format => "invalidformat" + get :using_defaults, format: "invalidformat" end end def test_invalid_variant + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + @request.variant = :invalid - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content + assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger end def test_variant_not_set_regular_template_missing - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content end def test_variant_with_implicit_rendering + @request.variant = :implicit + get :variant_with_implicit_rendering + assert_response :no_content + end + + def test_variant_with_implicit_template_rendering @request.variant = :mobile get :variant_with_implicit_rendering assert_equal "text/html", @response.content_type diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 246ba099af..710c428dcc 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -31,6 +31,15 @@ module BareMetalTest controller.index assert_equal ["Hello world"], controller.response_body end + + test "connect a request to controller instance without dispatch" do + env = {} + controller = BareController.new + controller.set_request! ActionDispatch::Request.new(env) + assert controller.request + assert controller.response + assert env['action_controller.instance'] + end end class HeadController < ActionController::Metal diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb index 5fd5946619..c8166280fc 100644 --- a/actionpack/test/controller/new_base/content_negotiation_test.rb +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -15,12 +15,12 @@ module ContentNegotiation class TestContentNegotiation < Rack::TestCase test "A */* Accept header will return HTML" do - get "/content_negotiation/basic/hello", {}, "HTTP_ACCEPT" => "*/*" + get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "*/*" } assert_body "Hello world */*!" end test "Not all mimes are converted to symbol" do - get "/content_negotiation/basic/all", {}, "HTTP_ACCEPT" => "text/plain, mime/another" + get "/content_negotiation/basic/all", headers: { "HTTP_ACCEPT" => "text/plain, mime/another" } assert_body '[:text, "mime/another"]' 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 9b57641e75..88453988dd 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -76,7 +76,7 @@ module ContentType end test "sets Content-Type as application/xml when rendering *.xml.erb" do - get "/content_type/implied/i_am_xml_erb", "format" => "xml" + get "/content_type/implied/i_am_xml_erb", params: { "format" => "xml" } assert_header "Content-Type", "application/xml; charset=utf-8" end @@ -88,7 +88,7 @@ module ContentType end test "sets Content-Type as application/xml when rendering *.xml.builder" do - get "/content_type/implied/i_am_xml_builder", "format" => "xml" + get "/content_type/implied/i_am_xml_builder", params: { "format" => "xml" } assert_header "Content-Type", "application/xml; charset=utf-8" end diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb index 45a6619eb4..537b93387a 100644 --- a/actionpack/test/controller/new_base/metal_test.rb +++ b/actionpack/test/controller/new_base/metal_test.rb @@ -18,8 +18,6 @@ module MetalTest end class TestMiddleware < ActiveSupport::TestCase - include RackTestUtils - def setup @app = Rack::Builder.new do use MetalTest::MetalMiddleware @@ -31,14 +29,14 @@ module MetalTest env = Rack::MockRequest.env_for("/authed") response = @app.call(env) - assert_equal "Hello World", body_to_string(response[2]) + 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!", body_to_string(response[2]) + assert_equal ["Not authed!"], response[2] assert_equal 401, response[0] end end diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb index 6b7b5e10e3..a30e937bb3 100644 --- a/actionpack/test/controller/new_base/middleware_test.rb +++ b/actionpack/test/controller/new_base/middleware_test.rb @@ -75,7 +75,7 @@ module MiddlewareTest test "middleware that is 'use'd is called as part of the Rack application" do result = @app.call(env_for("/")) - assert_equal "Hello World", RackTestUtils.body_to_string(result[2]) + assert_equal ["Hello World"], result[2] assert_equal "Success", result[1]["Middleware-Test"] end diff --git a/actionpack/test/controller/new_base/render_action_test.rb b/actionpack/test/controller/new_base/render_action_test.rb index 475bf9d3c9..3bf1dd0ede 100644 --- a/actionpack/test/controller/new_base/render_action_test.rb +++ b/actionpack/test/controller/new_base/render_action_test.rb @@ -88,7 +88,7 @@ module RenderAction test "rendering with layout => true" do assert_raise(ArgumentError) do - get "/render_action/basic/hello_world_with_layout", {}, "action_dispatch.show_exceptions" => false + get "/render_action/basic/hello_world_with_layout", headers: { "action_dispatch.show_exceptions" => false } end end @@ -108,7 +108,7 @@ module RenderAction test "rendering with layout => 'greetings'" do assert_raise(ActionView::MissingTemplate) do - get "/render_action/basic/hello_world_with_custom_layout", {}, "action_dispatch.show_exceptions" => false + get "/render_action/basic/hello_world_with_custom_layout", headers: { "action_dispatch.show_exceptions" => false } end end end diff --git a/actionpack/test/controller/new_base/render_layout_test.rb b/actionpack/test/controller/new_base/render_layout_test.rb index 4ac40ca405..7ab3777026 100644 --- a/actionpack/test/controller/new_base/render_layout_test.rb +++ b/actionpack/test/controller/new_base/render_layout_test.rb @@ -86,7 +86,7 @@ module ControllerLayouts XML_INSTRUCT = %Q(<?xml version="1.0" encoding="UTF-8"?>\n) test "if XML is selected, an HTML template is not also selected" do - get :index, :format => "xml" + get :index, params: { format: "xml" } assert_response XML_INSTRUCT end @@ -96,7 +96,7 @@ module ControllerLayouts end test "a layout for JS is ignored even if explicitly provided for HTML" do - get :explicit, { :format => "js" } + get :explicit, params: { format: "js" } assert_response "alert('foo');" end end @@ -120,7 +120,7 @@ module ControllerLayouts testing ControllerLayouts::FalseLayoutMethodController test "access false layout returned by a method/proc" do - get :index, :format => "js" + get :index, params: { format: "js" } assert_response "alert('foo');" end end diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb index 4c9126ca8c..9ea056194a 100644 --- a/actionpack/test/controller/new_base/render_streaming_test.rb +++ b/actionpack/test/controller/new_base/render_streaming_test.rb @@ -97,7 +97,7 @@ module RenderStreaming end test "do not stream on HTTP/1.0" do - get "/render_streaming/basic/hello_world", nil, "HTTP_VERSION" => "HTTP/1.0" + get "/render_streaming/basic/hello_world", headers: { "HTTP_VERSION" => "HTTP/1.0" } assert_body "Hello world, I'm here!" assert_status 200 assert_equal "22", headers["Content-Length"] diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 42a86b1d0d..b06ce5db40 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -111,7 +111,7 @@ module RenderTemplate end test "rendering a builder template" do - get :builder_template, "format" => "xml" + get :builder_template, params: { "format" => "xml" } assert_response "<html>\n <p>Hello</p>\n</html>\n" end @@ -126,7 +126,7 @@ module RenderTemplate assert_body "Hello <strong>this is also raw</strong> in an html template" assert_status 200 - get :with_implicit_raw, format: 'text' + get :with_implicit_raw, params: { format: 'text' } assert_body "Hello <strong>this is also raw</strong> in a text template" assert_status 200 @@ -186,21 +186,21 @@ module RenderTemplate end end - test "rendering with layout => :true" do + test "rendering with layout => true" do get "/render_template/with_layout/with_layout" assert_body "Hello from basic.html.erb, I'm here!" assert_status 200 end - test "rendering with layout => :false" do + test "rendering with layout => false" do get "/render_template/with_layout/with_layout_false" assert_body "Hello from basic.html.erb" assert_status 200 end - test "rendering with layout => :nil" do + test "rendering with layout => nil" do get "/render_template/with_layout/with_layout_nil" assert_body "Hello from basic.html.erb" diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index 5635e16234..11a19ab783 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -74,7 +74,7 @@ module Render end assert_raises(AbstractController::DoubleRenderError) do - get "/render/double_render", {}, "action_dispatch.show_exceptions" => false + get "/render/double_render", headers: { "action_dispatch.show_exceptions" => false } end end end @@ -84,13 +84,13 @@ module Render # Only public methods on actual controllers are callable actions test "raises an exception when a method of Object is called" do assert_raises(AbstractController::ActionNotFound) do - get "/render/blank_render/clone", {}, "action_dispatch.show_exceptions" => false + get "/render/blank_render/clone", headers: { "action_dispatch.show_exceptions" => false } end end test "raises an exception when a private method is called" do assert_raises(AbstractController::ActionNotFound) do - get "/render/blank_render/secretz", {}, "action_dispatch.show_exceptions" => false + get "/render/blank_render/secretz", headers: { "action_dispatch.show_exceptions" => false } end end end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index 059f310d49..59be08db54 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'action_controller/metal/strong_parameters' +require 'minitest/mock' class AlwaysPermittedParametersTest < ActiveSupport::TestCase def setup @@ -14,7 +15,13 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do assert_deprecated do - ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + end + end + + test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do + ActionController::Parameters.superclass.stub :const_missing, "super" do + assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT end end diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 645ecae220..8bf016d060 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -40,7 +40,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_filtered_parameters with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_equal @request.filtered_parameters, { 'controller' => 'params_wrapper_test/users', 'action' => 'parse', 'username' => 'sikachu', 'user' => { 'username' => 'sikachu' } } end end @@ -48,7 +48,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_derived_name_from_controller with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({ 'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }}) end end @@ -58,7 +58,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters :person @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({ 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }}) end end @@ -68,7 +68,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters Person @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({ 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }}) end end @@ -78,7 +78,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters :include => :username @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) end end @@ -88,7 +88,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters :exclude => :title @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) end end @@ -98,7 +98,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters :person, :include => :username @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end @@ -106,7 +106,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_not_enabled_format with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/xml' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer' }) end end @@ -115,7 +115,7 @@ class ParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do UsersController.wrap_parameters false @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer' }) end end @@ -125,7 +125,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters :format => :xml @request.env['CONTENT_TYPE'] = 'application/xml' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }}) end end @@ -133,7 +133,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_not_wrap_reserved_parameters with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'authenticity_token' => 'pwned', '_method' => 'put', 'utf8' => '☃', 'username' => 'sikachu' } + post :parse, params: { 'authenticity_token' => 'pwned', '_method' => 'put', 'utf8' => '☃', 'username' => 'sikachu' } assert_parameters({ 'authenticity_token' => 'pwned', '_method' => 'put', 'utf8' => '☃', 'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }}) end end @@ -141,7 +141,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_no_double_wrap_if_key_exists with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'user' => { 'username' => 'sikachu' }} + post :parse, params: { 'user' => { 'username' => 'sikachu' }} assert_parameters({ 'user' => { 'username' => 'sikachu' }}) end end @@ -149,7 +149,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_nested_params with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'person' => { 'username' => 'sikachu' }} + post :parse, params: { 'person' => { 'username' => 'sikachu' }} assert_parameters({ 'person' => { 'username' => 'sikachu' }, 'user' => {'person' => { 'username' => 'sikachu' }}}) end end @@ -160,7 +160,7 @@ class ParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) end end @@ -173,7 +173,7 @@ class ParamsWrapperTest < ActionController::TestCase UsersController.wrap_parameters Person @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end @@ -184,7 +184,7 @@ class ParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }}) end end @@ -192,7 +192,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_preserves_query_string_params with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - get :parse, { 'user' => { 'username' => 'nixon' } } + get :parse, params: { 'user' => { 'username' => 'nixon' } } assert_parameters( {'user' => { 'username' => 'nixon' } } ) @@ -202,7 +202,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_empty_parameter_set with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, {} + post :parse, params: {} assert_parameters( {'user' => { } } ) @@ -249,7 +249,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase def test_derived_name_from_controller with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }}) end end @@ -259,7 +259,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase begin with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) end ensure @@ -272,7 +272,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase begin with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'title' => 'Developer' }}) end ensure @@ -299,7 +299,7 @@ class AnonymousControllerParamsWrapperTest < ActionController::TestCase def test_does_not_implicitly_wrap_params with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({ 'username' => 'sikachu' }) end end @@ -308,7 +308,7 @@ class AnonymousControllerParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do @controller.class.wrap_parameters(:name => "guest") @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu' } + post :parse, params: { 'username' => 'sikachu' } assert_parameters({ 'username' => 'sikachu', 'guest' => { 'username' => 'sikachu' }}) end end @@ -344,7 +344,7 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' } + post :parse, params: { 'username' => 'sikachu', 'test_attr' => 'test_value' } assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }}) end end diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb index f46249d712..7136fafae5 100644 --- a/actionpack/test/controller/permitted_params_test.rb +++ b/actionpack/test/controller/permitted_params_test.rb @@ -14,12 +14,12 @@ class ActionControllerPermittedParamsTest < ActionController::TestCase tests PeopleController test "parameters are forbidden" do - post :create, { person: { name: "Mjallo!" } } + post :create, params: { person: { name: "Mjallo!" } } assert_equal "forbidden", response.body end test "parameters can be permitted and are then not forbidden" do - post :create_with_permit, { person: { name: "Mjallo!" } } + post :create_with_permit, params: { person: { name: "Mjallo!" } } assert_equal "permitted", response.body end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 103ca9c776..4f5ca46b04 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -1,8 +1,5 @@ require 'abstract_unit' -class WorkshopsController < ActionController::Base -end - class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls @@ -53,17 +50,12 @@ 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 def redirect_to_url_with_unescaped_query_string - redirect_to "http://dev.rubyonrails.org/query?status=new" + redirect_to "http://example.com/query?status=new" end def redirect_to_url_with_complex_scheme @@ -218,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 @@ -233,7 +219,7 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_url_with_unescaped_query_string get :redirect_to_url_with_unescaped_query_string assert_response :redirect - assert_redirected_to "http://dev.rubyonrails.org/query?status=new" + assert_redirected_to "http://example.com/query?status=new" end def test_redirect_to_url_with_complex_scheme diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb index d550422a2f..6b661de064 100644 --- a/actionpack/test/controller/render_js_test.rb +++ b/actionpack/test/controller/render_js_test.rb @@ -22,13 +22,13 @@ class RenderJSTest < ActionController::TestCase tests TestController def test_render_vanilla_js - xhr :get, :render_vanilla_js_hello + get :render_vanilla_js_hello, xhr: true assert_equal "alert('hello')", @response.body assert_equal "text/javascript", @response.content_type end def test_should_render_js_partial - xhr :get, :show_partial, :format => 'js' + get :show_partial, format: 'js', xhr: true assert_equal 'partial js', @response.body end end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index ada978aa11..b1ad16bc55 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase end def test_render_json_with_callback - xhr :get, :render_json_hello_world_with_callback + get :render_json_hello_world_with_callback, xhr: true assert_equal '/**/alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end def test_render_json_with_custom_content_type - xhr :get, :render_json_with_custom_content_type + get :render_json_with_custom_content_type, xhr: true assert_equal '{"hello":"world"}', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 929b161eb6..c9c43de37d 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -24,6 +24,11 @@ class TestControllerWithExtraEtags < ActionController::Base end end +class ImplicitRenderTestController < ActionController::Base + def empty_action + end +end + class TestController < ActionController::Base protect_from_forgery @@ -58,24 +63,24 @@ class TestController < ActionController::Base end end - def conditional_hello_with_public_header - if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true) - render :action => 'hello_world' + class Collection + def initialize(records) + @records = records end - end - - def conditional_hello_with_public_header_with_record - record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123") - if stale?(record, :public => true) - render :action => 'hello_world' + def maximum(attribute) + @records.max_by(&attribute).public_send(attribute) end end - def conditional_hello_with_public_header_and_expires_at - expires_in 1.minute - if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true) - render :action => 'hello_world' + def conditional_hello_with_collection_of_records + ts = Time.now.utc.beginning_of_day + + record = Struct.new(:updated_at, :cache_key).new(ts, "foo/123") + old_record = Struct.new(:updated_at, :cache_key).new(ts - 1.day, "bar/123") + + if stale?(Collection.new([record, old_record])) + render action: 'hello_world' end end @@ -120,6 +125,10 @@ class TestController < ActionController::Base render :action => 'hello_world' end + def respond_with_empty_body + render nothing: true + end + def conditional_hello_with_bangs render :action => 'hello_world' end @@ -129,50 +138,6 @@ class TestController < ActionController::Base fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ]) end - def heading - head :ok - end - - # :ported: - def double_render - render :text => "hello" - render :text => "world" - end - - def double_redirect - redirect_to :action => "double_render" - redirect_to :action => "double_render" - end - - def render_and_redirect - render :text => "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}" - end - - def render_to_string_with_inline_and_render - render_to_string :inline => "<%= 'dlrow olleh'.reverse %>" - render :template => "test/hello_world" - end - - def rendering_with_conflicting_local_vars - @name = "David" - render :action => "potential_conflicts" - end - - def hello_world_from_rxml_using_action - render :action => "hello_world_from_rxml", :handlers => [:builder] - end - - # :deprecated: - def hello_world_from_rxml_using_template - render :template => "test/hello_world_from_rxml", :handlers => [:builder] - end - def head_created head :created end @@ -217,6 +182,11 @@ class TestController < ActionController::Base head :forbidden, :x_custom_header => "something" end + def head_and_return + head :ok and return + raise 'should not reach this line' + end + def head_with_no_content # Fill in the headers with dummy data to make # sure they get removed during the testing @@ -309,6 +279,12 @@ class ExpiresInRenderTest < ActionController::TestCase assert_match(/no-transform/, @response.headers["Cache-Control"]) end + def test_render_nothing_deprecated + assert_deprecated do + get :respond_with_empty_body + end + end + def test_date_header_when_expires_in time = Time.mktime(2011,10,30) Time.stubs(:now).returns(time) @@ -353,7 +329,6 @@ class LastModifiedRenderTest < ActionController::TestCase assert_equal @last_modified, @response.headers['Last-Modified'] end - def test_responds_with_last_modified_with_record get :conditional_hello_with_record assert_equal @last_modified, @response.headers['Last-Modified'] @@ -364,6 +339,7 @@ class LastModifiedRenderTest < ActionController::TestCase get :conditional_hello_with_record assert_equal 304, @response.status.to_i assert @response.body.blank? + assert_not_nil @response.etag assert_equal @last_modified, @response.headers['Last-Modified'] end @@ -382,6 +358,34 @@ class LastModifiedRenderTest < ActionController::TestCase assert_equal @last_modified, @response.headers['Last-Modified'] end + def test_responds_with_last_modified_with_collection_of_records + get :conditional_hello_with_collection_of_records + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_with_collection_of_records + @request.if_modified_since = @last_modified + get :conditional_hello_with_collection_of_records + assert_equal 304, @response.status.to_i + assert @response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_but_etag_differs_with_collection_of_records + @request.if_modified_since = @last_modified + @request.if_none_match = "234" + get :conditional_hello_with_collection_of_records + assert_response :success + end + + def test_request_modified_with_collection_of_records + @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT' + get :conditional_hello_with_collection_of_records + assert_equal 200, @response.status.to_i + assert @response.body.present? + assert_equal @last_modified, @response.headers['Last-Modified'] + end + def test_request_with_bang_gets_last_modified get :conditional_hello_with_bangs assert_equal @last_modified, @response.headers['Last-Modified'] @@ -464,6 +468,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 @@ -527,21 +540,21 @@ class HeadRenderTest < ActionController::TestCase end def test_head_with_symbolic_status - get :head_with_symbolic_status, :status => "ok" + get :head_with_symbolic_status, params: { status: "ok" } assert_equal 200, @response.status assert_response :ok - get :head_with_symbolic_status, :status => "not_found" + get :head_with_symbolic_status, params: { status: "not_found" } assert_equal 404, @response.status assert_response :not_found - get :head_with_symbolic_status, :status => "no_content" + get :head_with_symbolic_status, params: { status: "no_content" } assert_equal 204, @response.status assert !@response.headers.include?('Content-Length') assert_response :no_content Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code| - get :head_with_symbolic_status, :status => status.to_s + get :head_with_symbolic_status, params: { status: status.to_s } assert_equal code, @response.response_code assert_response status end @@ -549,7 +562,7 @@ class HeadRenderTest < ActionController::TestCase def test_head_with_integer_status Rack::Utils::HTTP_STATUS_CODES.each do |code, message| - get :head_with_integer_status, :status => code.to_s + get :head_with_integer_status, params: { status: code.to_s } assert_equal message, @response.message end end @@ -563,7 +576,7 @@ class HeadRenderTest < ActionController::TestCase end def test_head_with_string_status - get :head_with_string_status, :status => "404 Eat Dirt" + get :head_with_string_status, params: { status: "404 Eat Dirt" } assert_equal 404, @response.response_code assert_equal "Not Found", @response.message assert_response :not_found @@ -576,4 +589,63 @@ class HeadRenderTest < ActionController::TestCase assert_equal "something", @response.headers["X-Custom-Header"] assert_response :forbidden end + + def test_head_returns_truthy_value + assert_nothing_raised do + get :head_and_return + end + end +end + +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' + end + end + end + + tests HttpCacheForeverController + + def test_cache_with_public + get :cache_me_forever, params: {public: true} + assert_equal "max-age=#{100.years.to_i}, public", @response.headers["Cache-Control"] + assert_not_nil @response.etag + end + + def test_cache_with_private + get :cache_me_forever + assert_equal "max-age=#{100.years.to_i}, private", @response.headers["Cache-Control"] + assert_not_nil @response.etag + assert_response :success + end + + def test_cache_response_code_with_if_modified_since + get :cache_me_forever + assert_response :success + @request.if_modified_since = @response.headers['Last-Modified'] + get :cache_me_forever + assert_response :not_modified + end + + def test_cache_response_code_with_etag + get :cache_me_forever + assert_response :success + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever + assert_response :not_modified + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever, params: {version: 'v2'} + assert_response :success + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever, params: {version: 'v2'} + assert_response :not_modified + end end diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb index 4f280c4bec..7a91577b17 100644 --- a/actionpack/test/controller/render_xml_test.rb +++ b/actionpack/test/controller/render_xml_test.rb @@ -81,7 +81,7 @@ class RenderXmlTest < ActionController::TestCase end def test_should_render_formatted_xml_erb_template - get :formatted_xml_erb, :format => :xml + get :formatted_xml_erb, format: :xml assert_equal '<test>passed formatted xml erb</test>', @response.body end @@ -91,7 +91,7 @@ class RenderXmlTest < ActionController::TestCase end def test_should_use_implicit_content_type - get :implicit_content_type, :format => 'atom' + get :implicit_content_type, format: 'atom' assert_equal Mime::ATOM, @response.content_type end end diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb new file mode 100644 index 0000000000..b55a25430b --- /dev/null +++ b/actionpack/test/controller/renderer_test.rb @@ -0,0 +1,103 @@ +require 'abstract_unit' + +class RendererTest < ActiveSupport::TestCase + test 'creating with a controller' do + controller = CommentsController + renderer = ActionController::Renderer.for controller + + assert_equal controller, renderer.controller + end + + test 'creating from a controller' do + controller = AccountsController + renderer = controller.renderer + + assert_equal controller, renderer.controller + end + + test 'rendering with a class renderer' do + renderer = ApplicationController.renderer + content = renderer.render template: 'ruby_template' + + assert_equal 'Hello from Ruby code', content + end + + test 'rendering with an instance renderer' do + renderer = ApplicationController.renderer.new + content = renderer.render file: 'test/hello_world' + + assert_equal 'Hello world!', content + end + + test 'rendering with a controller class' do + assert_equal 'Hello world!', ApplicationController.render('test/hello_world') + end + + test 'rendering with locals' do + renderer = ApplicationController.renderer + content = renderer.render template: 'test/render_file_with_locals', + locals: { secret: 'bar' } + + assert_equal "The secret is bar\n", content + end + + test 'rendering with assigns' do + renderer = ApplicationController.renderer + content = renderer.render template: 'test/render_file_with_ivar', + assigns: { secret: 'foo' } + + assert_equal "The secret is foo\n", content + end + + test 'rendering with custom env' do + renderer = ApplicationController.renderer.new method: 'post' + content = renderer.render inline: '<%= request.post? %>' + + assert_equal 'true', content + end + + test 'rendering with defaults' do + renderer = ApplicationController.renderer + renderer.defaults[:https] = true + content = renderer.render inline: '<%= request.ssl? %>' + + assert_equal 'true', content + end + + test 'same defaults from the same controller' do + renderer_defaults = ->(controller) { controller.renderer.defaults } + + assert renderer_defaults[AccountsController].equal? renderer_defaults[AccountsController] + assert_not renderer_defaults[AccountsController].equal? renderer_defaults[CommentsController] + end + + test 'rendering with different formats' do + html = 'Hello world!' + xml = "<p>Hello world!</p>\n" + + assert_equal html, render['respond_to/using_defaults'] + assert_equal xml, render['respond_to/using_defaults.xml.builder'] + assert_equal xml, render['respond_to/using_defaults', formats: :xml] + end + + test 'rendering with helpers' do + assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>'] + end + + test 'rendering from inherited renderer' do + inherited = Class.new ApplicationController.renderer do + defaults[:script_name] = 'script' + def render(options) + super options.merge(locals: { param: :value }) + end + end + + template = '<%= url_for controller: :foo, action: :bar, param: param %>' + assert_equal 'script/foo/bar?param=value', inherited.render(inline: template) + end + + private + def render + @render ||= ApplicationController.renderer.method(:render) + end +end diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index e624f11773..77a2f68b1c 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -24,12 +24,4 @@ class ActionController::TestRequestTest < ActiveSupport::TestCase end end - def test_session_id_exists_by_default - assert_not_nil(@request.session_options[:id]) - end - - def test_session_id_different_on_each_call - assert_not_equal(@request.session_options[:id], ActionController::TestRequest.new.session_options[:id]) - end - end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 3e0bfe8d14..82c808754c 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -12,14 +12,6 @@ module RequestForgeryProtectionActions render :inline => "<%= button_to('New', '/') %>" end - def external_form - render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => 'external_token') {} %>" - end - - def external_form_without_protection - render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => false) {} %>" - end - def unsafe render :text => 'pwn' end @@ -28,14 +20,6 @@ module RequestForgeryProtectionActions render :inline => "<%= csrf_meta_tags %>" end - def external_form_for - render :inline => "<%= form_for(:some_resource, :authenticity_token => 'external_token') {} %>" - end - - def form_for_without_protection - render :inline => "<%= form_for(:some_resource, :authenticity_token => false ) {} %>" - end - def form_for_remote render :inline => "<%= form_for(:some_resource, :remote => true ) {} %>" end @@ -70,7 +54,6 @@ module RequestForgeryProtectionActions negotiate_same_origin end - def rescue_action(e) raise e end end # sample controllers @@ -89,17 +72,42 @@ class RequestForgeryProtectionControllerUsingNullSession < ActionController::Bas def signed cookies.signed[:foo] = 'bar' - render :nothing => true + head :ok end def encrypted cookies.encrypted[:foo] = 'bar' - render :nothing => true + head :ok end def try_to_reset_session reset_session - render :nothing => true + head :ok + end +end + +class PrependProtectForgeryBaseController < ActionController::Base + before_action :custom_action + attr_accessor :called_callbacks + + def index + render inline: 'OK' + end + + protected + + def add_called_callback(name) + @called_callbacks ||= [] + @called_callbacks << name + end + + + def custom_action + add_called_callback("custom_action") + end + + def verify_authenticity_token + add_called_callback("verify_authenticity_token") end end @@ -237,23 +245,23 @@ module RequestForgeryProtectionTests end def test_should_not_allow_xhr_post_without_token - assert_blocked { xhr :post, :index } + assert_blocked { post :index, xhr: true } end def test_should_allow_post_with_token - assert_not_blocked { post :index, :custom_authenticity_token => @token } + assert_not_blocked { post :index, params: { custom_authenticity_token: @token } } end def test_should_allow_patch_with_token - assert_not_blocked { patch :index, :custom_authenticity_token => @token } + assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } } end def test_should_allow_put_with_token - assert_not_blocked { put :index, :custom_authenticity_token => @token } + assert_not_blocked { put :index, params: { custom_authenticity_token: @token } } end def test_should_allow_delete_with_token - assert_not_blocked { delete :index, :custom_authenticity_token => @token } + assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } } end def test_should_allow_post_with_token_in_header @@ -315,21 +323,21 @@ module RequestForgeryProtectionTests get :negotiate_same_origin end - assert_cross_origin_not_blocked { xhr :get, :same_origin_js } - assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' } + assert_cross_origin_not_blocked { get :same_origin_js, xhr: true } + assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: 'js'} assert_cross_origin_not_blocked do @request.accept = 'text/javascript' - xhr :get, :negotiate_same_origin + get :negotiate_same_origin, xhr: true end end # Allow non-GET requests since GET is all a remote <script> tag can muster. def test_should_allow_non_get_js_without_xhr_header - assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token } - assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token } + assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } } + assert_cross_origin_not_blocked { post :same_origin_js, params: { format: 'js', custom_authenticity_token: @token } } assert_cross_origin_not_blocked do @request.accept = 'text/javascript' - post :negotiate_same_origin, custom_authenticity_token: @token + post :negotiate_same_origin, params: { custom_authenticity_token: @token} end end @@ -341,11 +349,18 @@ module RequestForgeryProtectionTests get :negotiate_cross_origin end - assert_cross_origin_not_blocked { xhr :get, :cross_origin_js } - assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true } + assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: 'js' } assert_cross_origin_not_blocked do @request.accept = 'text/javascript' - xhr :get, :negotiate_cross_origin + get :negotiate_cross_origin, xhr: true + end + end + + def test_should_not_raise_error_if_token_is_not_a_string + @controller.unstub(:valid_authenticity_token?) + assert_blocked do + patch :index, params: { custom_authenticity_token: { foo: 'bar' } } end end @@ -431,6 +446,41 @@ class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::T end end +class PrependProtectForgeryBaseControllerTest < ActionController::TestCase + PrependTrueController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery prepend: true + end + + PrependFalseController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery prepend: false + end + + PrependDefaultController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery + end + + def test_verify_authenticity_token_is_prepended + @controller = PrependTrueController.new + get :index + expected_callback_order = ["verify_authenticity_token", "custom_action"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end + + def test_verify_authenticity_token_is_not_prepended + @controller = PrependFalseController.new + get :index + expected_callback_order = ["custom_action", "verify_authenticity_token"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end + + def test_verify_authenticity_token_is_prepended_by_default + @controller = PrependDefaultController.new + get :index + expected_callback_order = ["verify_authenticity_token", "custom_action"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end +end + class FreeCookieControllerTest < ActionController::TestCase def setup @controller = FreeCookieController.new @@ -483,7 +533,7 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase @controller.stubs(:valid_authenticity_token?).returns(:true) begin - post :index, :custom_token_name => 'foobar' + post :index, params: { custom_token_name: 'foobar' } assert_equal 0, @logger.logged(:warn).size ensure ActionController::Base.logger = @old_logger @@ -494,7 +544,7 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase ActionController::Base.logger = @logger begin - post :index, :custom_token_name => 'bazqux' + post :index, params: { custom_token_name: 'bazqux' } assert_equal 1, @logger.logged(:warn).size ensure ActionController::Base.logger = @old_logger diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 6803dbbb62..a901e56332 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -12,21 +12,21 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase test "missing required parameters will raise exception" do assert_raise ActionController::ParameterMissing do - post :create, { magazine: { name: "Mjallo!" } } + post :create, params: { magazine: { name: "Mjallo!" } } end assert_raise ActionController::ParameterMissing do - post :create, { book: { title: "Mjallo!" } } + post :create, params: { book: { title: "Mjallo!" } } end end test "required parameters that are present will not raise" do - post :create, { book: { name: "Mjallo!" } } + post :create, params: { book: { name: "Mjallo!" } } assert_response :ok end test "required parameters with false value will not raise" do - post :create, { book: { name: false } } + post :create, params: { book: { name: false } } assert_response :ok end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 0e15883f43..f3da2df3ef 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1047,6 +1047,28 @@ class ResourcesTest < ActionController::TestCase end end + def test_assert_routing_accepts_all_as_a_valid_method + with_routing do |set| + set.draw do + match "/products", to: "products#show", via: :all + end + + assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" }) + end + end + + def test_assert_routing_fails_when_not_all_http_methods_are_recognized + with_routing do |set| + set.draw do + match "/products", to: "products#show", via: [:get, :post, :put] + end + + assert_raises(Minitest::Assertion) do + assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" }) + end + end + end + def test_singleton_resource_name_is_not_singularized with_singleton_resources(:preferences) do assert_singleton_restful_for :preferences @@ -1184,10 +1206,10 @@ class ResourcesTest < ActionController::TestCase end @controller = "#{options[:options][:controller].camelize}Controller".constantize.new - @controller.singleton_class.send(:include, @routes.url_helpers) + @controller.singleton_class.include(@routes.url_helpers) @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new - get :index, options[:options] + get :index, params: options[:options] options[:options].delete :action path = "#{options[:as] || controller_name}" @@ -1254,10 +1276,10 @@ class ResourcesTest < ActionController::TestCase 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 - @controller.singleton_class.send(:include, @routes.url_helpers) + @controller.singleton_class.include(@routes.url_helpers) @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new - get :show, options[:options] + get :show, params: options[:options] options[:options].delete :action full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9caa5cbe57..9bbfb74e72 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/core_ext/object/with_options' @@ -9,8 +8,6 @@ class MilestonesController < ActionController::Base alias_method :show, :index end -ROUTING = ActionDispatch::Routing - # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < ActiveSupport::TestCase include RoutingTestHelpers @@ -872,7 +869,7 @@ class RouteSetTest < ActiveSupport::TestCase def default_route_set @default_route_set ||= begin - set = ROUTING::RouteSet.new + set = ActionDispatch::Routing::RouteSet.new set.draw do get '/:controller(/:action(/:id))' end @@ -1749,13 +1746,13 @@ class RouteSetTest < ActiveSupport::TestCase include ActionDispatch::RoutingVerbs - class TestSet < ROUTING::RouteSet + class TestSet < ActionDispatch::Routing::RouteSet def initialize(block) @block = block super() end - class Dispatcher < ROUTING::RouteSet::Dispatcher + class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher def initialize(defaults, set, block) super(defaults) @block = block diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index c002cf4d8f..36c57ec9b2 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' module TestFileUtils diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index f7eba1ef43..786dc15444 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -58,13 +58,13 @@ module ShowExceptions class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest test 'show error page' do @app = ShowExceptionsOverriddenController.action(:boom) - get '/', {'detailed' => '0'} + get '/', params: { 'detailed' => '0' } assert_equal "500 error fixture\n", body end test 'show diagnostics message' do @app = ShowExceptionsOverriddenController.action(:boom) - get '/', {'detailed' => '1'} + get '/', params: { 'detailed' => '1' } assert_match(/boom/, body) end end @@ -72,23 +72,23 @@ module ShowExceptions class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest def test_render_json_exception @app = ShowExceptionsOverriddenController.action(:boom) - get "/", {}, 'HTTP_ACCEPT' => 'application/json' + get "/", headers: { 'HTTP_ACCEPT' => 'application/json' } assert_response :internal_server_error assert_equal 'application/json', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_json, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_json, response.body) end def test_render_xml_exception @app = ShowExceptionsOverriddenController.action(:boom) - get "/", {}, 'HTTP_ACCEPT' => 'application/xml' + get "/", headers: { 'HTTP_ACCEPT' => 'application/xml' } assert_response :internal_server_error assert_equal 'application/xml', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_xml, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_xml, response.body) end def test_render_fallback_exception @app = ShowExceptionsOverriddenController.action(:boom) - get "/", {}, 'HTTP_ACCEPT' => 'text/csv' + get "/", headers: { 'HTTP_ACCEPT' => 'text/csv' } assert_response :internal_server_error assert_equal 'text/html', response.content_type.to_s end @@ -101,7 +101,7 @@ module ShowExceptions @app.instance_variable_set(:@exceptions_app, nil) $stderr = StringIO.new - get '/', {}, 'HTTP_ACCEPT' => 'text/json' + get '/', headers: { 'HTTP_ACCEPT' => 'text/json' } assert_response :internal_server_error assert_equal 'text/plain', response.content_type.to_s ensure diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 2e1f21c645..86ffb898ad 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -6,57 +6,70 @@ require 'rails/engine' class TestCaseTest < ActionController::TestCase class TestController < ActionController::Base def no_op - render :text => 'dummy' + render text: 'dummy' end def set_flash flash["test"] = ">#{flash["test"]}<" + render text: 'ignore me' + end + + def delete_flash + flash.delete("test") render :text => 'ignore me' end def set_flash_now flash.now["test_now"] = ">#{flash["test_now"]}<" - render :text => 'ignore me' + render text: 'ignore me' end def set_session session['string'] = 'A wonder' session[:symbol] = 'it works' - render :text => 'Success' + render text: 'Success' end def reset_the_session reset_session - render :text => 'ignore me' + render text: '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 text: request.raw_post end def render_body - render :text => request.body.read + render text: request.body.read end def test_params - render :text => params.inspect + render text: params.inspect + end + + def test_query_parameters + render text: request.query_parameters.inspect + end + + def test_request_parameters + render text: request.request_parameters.inspect end def test_uri - render :text => request.fullpath + render text: request.fullpath end def test_format - render :text => request.format + render text: request.format end def test_query_string - render :text => request.query_string + render text: request.query_string end def test_protocol - render :text => request.protocol + render text: request.protocol end def test_headers @@ -64,7 +77,7 @@ class TestCaseTest < ActionController::TestCase end def test_html_output - render :text => <<HTML + render text: <<HTML <html> <body> <a href="/"><img src="/images/button.png" /></a> @@ -86,7 +99,7 @@ HTML def test_xml_output response.content_type = "application/xml" - render :text => <<XML + render text: <<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> @@ -95,15 +108,15 @@ XML end def test_only_one_param - render :text => (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" + render text: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" end def test_remote_addr - render :text => (request.remote_addr || "not specified") + render text: (request.remote_addr || "not specified") end def test_file_upload - render :text => params[:file].size + render text: params[:file].size end def test_send_file @@ -111,26 +124,20 @@ XML end def redirect_to_same_controller - redirect_to :controller => 'test', :action => 'test_uri', :id => 5 + redirect_to controller: 'test', action: 'test_uri', id: 5 end def redirect_to_different_controller - redirect_to :controller => 'fail', :id => 5 + redirect_to controller: 'fail', id: 5 end def create - head :created, :location => 'created resource' + head :created, location: 'created resource' end def delete_cookie cookies.delete("foo") - render :nothing => true - end - - def test_assigns - @foo = "foo" - @foo_hash = {:foo => :bar} - render :nothing => true + head :ok end def test_without_body @@ -144,7 +151,7 @@ XML private def generate_url(opts) - url_for(opts.merge(:action => "test_uri")) + url_for(opts.merge(action: "test_uri")) end end @@ -161,17 +168,6 @@ XML end end - class ViewAssignsController < ActionController::Base - def test_assigns - @foo = "foo" - render :nothing => true - end - - def view_assigns - { "bar" => "bar" } - end - end - class DefaultUrlOptionsCachingController < ActionController::Base before_action { @dynamic_opt = 'opt' } @@ -209,32 +205,50 @@ XML end def test_raw_post_handling - params = Hash[:page, {:name => 'page name'}, 'some key', 123] - post :render_raw_post, params.dup + params = Hash[:page, { name: 'page name' }, 'some key', 123] + post :render_raw_post, params: params.dup assert_equal params.to_query, @response.body end def test_body_stream - params = Hash[:page, { :name => 'page name' }, 'some key', 123] + params = Hash[:page, { name: 'page name' }, 'some key', 123] - post :render_body, params.dup + post :render_body, params: params.dup + + assert_equal params.to_query, @response.body + end + + def test_deprecated_body_stream + params = Hash[:page, { name: 'page name' }, 'some key', 123] + + assert_deprecated { post :render_body, params.dup } assert_equal params.to_query, @response.body end def test_document_body_and_params_with_post - post :test_params, :id => 1 - assert_equal("{\"id\"=>\"1\", \"controller\"=>\"test_case_test/test\", \"action\"=>\"test_params\"}", @response.body) + post :test_params, params: { id: 1 } + assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) end def test_document_body_with_post - post :render_body, "document body" + post :render_body, body: "document body" + assert_equal "document body", @response.body + end + + def test_deprecated_document_body_with_post + assert_deprecated { post :render_body, "document body" } assert_equal "document body", @response.body end def test_document_body_with_put - put :render_body, "document body" + put :render_body, body: "document body" + assert_equal "document body", @response.body + end + + def test_deprecated_document_body_with_put + assert_deprecated { put :render_body, "document body" } assert_equal "document body", @response.body end @@ -243,25 +257,42 @@ XML assert_equal 200, @response.status end - def test_head_params_as_string - assert_raise(NoMethodError) { head :test_params, "document body", :id => 10 } - end - def test_process_without_flash process :set_flash assert_equal '><', flash['test'] end + def test_deprecated_process_with_flash + assert_deprecated { process :set_flash, "GET", nil, nil, { "test" => "value" } } + assert_equal '>value<', flash['test'] + end + def test_process_with_flash - process :set_flash, "GET", nil, nil, { "test" => "value" } + process :set_flash, + method: "GET", + flash: { "test" => "value" } assert_equal '>value<', flash['test'] end + def test_deprecated_process_with_flash_now + assert_deprecated { process :set_flash_now, "GET", nil, nil, { "test_now" => "value_now" } } + assert_equal '>value_now<', flash['test_now'] + end + def test_process_with_flash_now - process :set_flash_now, "GET", nil, nil, { "test_now" => "value_now" } + process :set_flash_now, + method: "GET", + flash: { "test_now" => "value_now" } assert_equal '>value_now<', flash['test_now'] end + def test_process_delete_flash + process :set_flash + process :delete_flash + assert_empty flash + assert_empty session + end + def test_process_with_session process :set_session assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key" @@ -271,22 +302,48 @@ XML end def test_process_with_session_arg - process :no_op, "GET", nil, { 'string' => 'value1', :symbol => 'value2' } + assert_deprecated { process :no_op, "GET", nil, { 'string' => 'value1', symbol: 'value2' } } assert_equal 'value1', session['string'] assert_equal 'value1', session[:string] assert_equal 'value2', session['symbol'] assert_equal 'value2', session[:symbol] end + def test_process_with_session_kwarg + process :no_op, method: "GET", session: { 'string' => 'value1', symbol: 'value2' } + assert_equal 'value1', session['string'] + assert_equal 'value1', session[:string] + assert_equal 'value2', session['symbol'] + assert_equal 'value2', session[:symbol] + end + + def test_deprecated_process_merges_session_arg + session[:foo] = 'bar' + assert_deprecated { + get :no_op, nil, { bar: 'baz' } + } + assert_equal 'bar', session[:foo] + assert_equal 'baz', session[:bar] + end + def test_process_merges_session_arg session[:foo] = 'bar' - get :no_op, nil, { :bar => 'baz' } + get :no_op, session: { bar: 'baz' } assert_equal 'bar', session[:foo] assert_equal 'baz', session[:bar] end + def test_deprecated_merged_session_arg_is_retained_across_requests + assert_deprecated { + get :no_op, nil, { foo: 'bar' } + } + assert_equal 'bar', session[:foo] + get :no_op + assert_equal 'bar', session[:foo] + end + def test_merged_session_arg_is_retained_across_requests - get :no_op, nil, { :foo => 'bar' } + get :no_op, session: { foo: 'bar' } assert_equal 'bar', session[:foo] get :no_op assert_equal 'bar', session[:foo] @@ -294,7 +351,7 @@ XML def test_process_overwrites_existing_session_arg session[:foo] = 'bar' - get :no_op, nil, { :foo => 'baz' } + get :no_op, session: { foo: 'baz' } assert_equal 'baz', session[:foo] end @@ -321,19 +378,40 @@ XML assert_equal "/test_case_test/test/test_uri", @response.body end + def test_process_with_symbol_method + process :test_uri, method: :get + assert_equal "/test_case_test/test/test_uri", @response.body + end + + def test_deprecated_process_with_request_uri_with_params + assert_deprecated { process :test_uri, "GET", id: 7 } + assert_equal "/test_case_test/test/test_uri/7", @response.body + end + def test_process_with_request_uri_with_params - process :test_uri, "GET", :id => 7 + process :test_uri, + method: "GET", + params: { id: 7 } + assert_equal "/test_case_test/test/test_uri/7", @response.body end + def test_deprecated_process_with_request_uri_with_params_with_explicit_uri + @request.env['PATH_INFO'] = "/explicit/uri" + assert_deprecated { process :test_uri, "GET", id: 7 } + assert_equal "/explicit/uri", @response.body + end + def test_process_with_request_uri_with_params_with_explicit_uri @request.env['PATH_INFO'] = "/explicit/uri" - process :test_uri, "GET", :id => 7 + process :test_uri, method: "GET", params: { id: 7 } assert_equal "/explicit/uri", @response.body end def test_process_with_query_string - process :test_query_string, "GET", :q => 'test' + process :test_query_string, + method: "GET", + params: { q: 'test' } assert_equal "q=test", @response.body end @@ -345,36 +423,12 @@ XML end def test_multiple_calls - process :test_only_one_param, "GET", :left => true + process :test_only_one_param, method: "GET", params: { left: true } assert_equal "OK", @response.body - process :test_only_one_param, "GET", :right => true + process :test_only_one_param, method: "GET", params: { right: true } 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 @@ -390,21 +444,21 @@ XML end def test_assert_generates - assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5' - assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"} - assert_generates 'controller/action/5', {:controller => "controller", :action => "action", :id => "5", :name => "bob"}, {}, {:name => "bob"} - assert_generates 'controller/action/7', {:id => "7", :name => "bob"}, {:controller => "controller", :action => "action"}, {:name => "bob"} - assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action", :name => "bob"}, {} + assert_generates 'controller/action/5', controller: 'controller', action: 'action', id: '5' + assert_generates 'controller/action/7', { id: "7" }, { controller: "controller", action: "action" } + assert_generates 'controller/action/5', { controller: "controller", action: "action", id: "5", name: "bob" }, {}, { name: "bob" } + assert_generates 'controller/action/7', { id: "7", name: "bob" }, { controller: "controller", action: "action" }, { name: "bob" } + assert_generates 'controller/action/7', { id: "7" }, { controller: "controller", action: "action", name: "bob" }, {} end def test_assert_routing - assert_routing 'content', :controller => 'content', :action => 'index' + assert_routing 'content', controller: 'content', action: 'index' end def test_assert_routing_with_method with_routing do |set| set.draw { resources(:content) } - assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' }) + assert_routing({ method: 'post', path: 'content' }, { controller: 'content', action: 'create' }) end end @@ -416,29 +470,87 @@ XML end end - assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' + assert_routing 'admin/user', controller: 'admin/user', action: 'index' end end def test_assert_routing_with_glob with_routing do |set| set.draw { get('*path' => "pages#show") } - assert_routing('/company/about', { :controller => 'pages', :action => 'show', :path => 'company/about' }) + assert_routing('/company/about', { controller: 'pages', action: 'show', path: 'company/about' }) end end + def test_deprecated_params_passing + assert_deprecated { + get :test_params, page: { name: "Page name", month: '4', year: '2004', day: '6' } + } + parsed_params = eval(@response.body) + assert_equal( + { + 'controller' => 'test_case_test/test', 'action' => 'test_params', + 'page' => { 'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6' } + }, + parsed_params + ) + end + def test_params_passing - get :test_params, :page => {:name => "Page name", :month => '4', :year => '2004', :day => '6'} + get :test_params, params: { + page: { + name: "Page name", + month: '4', + year: '2004', + day: '6' + } + } + parsed_params = eval(@response.body) + assert_equal( + { + 'controller' => 'test_case_test/test', 'action' => 'test_params', + 'page' => { 'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6' } + }, + parsed_params + ) + end + + def test_query_param_named_action + get :test_query_parameters, params: {action: 'foobar'} + parsed_params = eval(@response.body) + assert_equal({action: 'foobar'}, parsed_params) + end + + def test_request_param_named_action + post :test_request_parameters, params: {action: 'foobar'} + parsed_params = eval(@response.body) + assert_equal({'action' => 'foobar'}, parsed_params) + end + + def test_kwarg_params_passing_with_session_and_flash + get :test_params, params: { + page: { + name: "Page name", + month: '4', + year: '2004', + day: '6' + } + }, session: { 'foo' => 'bar' }, flash: { notice: 'created' } + parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, parsed_params ) + + assert_equal 'bar', session[:foo] + assert_equal 'created', flash[:notice] end def test_params_passing_with_fixnums - get :test_params, :page => {:name => "Page name", :month => 4, :year => 2004, :day => 6} + get :test_params, params: { + page: { name: "Page name", month: 4, year: 2004, day: 6 } + } parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -448,7 +560,7 @@ XML end def test_params_passing_with_fixnums_when_not_html_request - get :test_params, :format => 'json', :count => 999 + get :test_params, params: { format: 'json', count: 999 } parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -458,7 +570,17 @@ XML end def test_params_passing_path_parameter_is_string_when_not_html_request - get :test_params, :format => 'json', :id => 1 + get :test_params, params: { format: 'json', id: 1 } + parsed_params = eval(@response.body) + assert_equal( + {'controller' => 'test_case_test/test', 'action' => 'test_params', + 'format' => 'json', 'id' => '1' }, + parsed_params + ) + end + + def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request + assert_deprecated { get :test_params, format: 'json', id: 1 } parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -469,7 +591,9 @@ XML def test_params_passing_with_frozen_values assert_nothing_raised do - get :test_params, :frozen => 'icy'.freeze, :frozens => ['icy'.freeze].freeze, :deepfreeze => { :frozen => 'icy'.freeze }.freeze + get :test_params, params: { + frozen: 'icy'.freeze, frozens: ['icy'.freeze].freeze, deepfreeze: { frozen: 'icy'.freeze }.freeze + } end parsed_params = eval(@response.body) assert_equal( @@ -480,8 +604,8 @@ XML end def test_params_passing_doesnt_modify_in_place - page = {:name => "Page name", :month => 4, :year => 2004, :day => 6} - get :test_params, :page => page + page = { name: "Page name", month: 4, year: 2004, day: 6 } + get :test_params, params: { page: page } assert_equal 2004, page[:year] end @@ -504,25 +628,32 @@ XML end def test_id_converted_to_string - get :test_params, :id => 20, :foo => Object.new + get :test_params, params: { + id: 20, foo: Object.new + } + assert_kind_of String, @request.path_parameters[:id] + end + + def test_deprecared_id_converted_to_string + assert_deprecated { get :test_params, id: 20, foo: Object.new} assert_kind_of String, @request.path_parameters[:id] end def test_array_path_parameter_handled_properly with_routing do |set| set.draw do - get 'file/*path', :to => 'test_case_test/test#test_params' + get 'file/*path', to: 'test_case_test/test#test_params' get ':controller/:action' end - get :test_params, :path => ['hello', 'world'] + get :test_params, params: { path: ['hello', 'world'] } assert_equal ['hello', 'world'], @request.path_parameters[:path] assert_equal 'hello/world', @request.path_parameters[:path].to_param end end def test_assert_realistic_path_parameters - get :test_params, :id => 20, :foo => Object.new + get :test_params, params: { id: 20, foo: Object.new } # All elements of path_parameters should use Symbol keys @request.path_parameters.each_key do |key| @@ -554,19 +685,57 @@ XML end def test_header_properly_reset_after_remote_http_request - xhr :get, :test_params + get :test_params, xhr: true assert_nil @request.env['HTTP_X_REQUESTED_WITH'] assert_nil @request.env['HTTP_ACCEPT'] end + def test_deprecated_xhr_with_params + assert_deprecated { xhr :get, :test_params, params: { id: 1 } } + + assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @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) + end + + def test_xhr_with_session + get :set_session, xhr: true + + assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key" + assert_equal 'A wonder', session[:string], "Test session hash should allow indifferent access" + assert_equal 'it works', session['symbol'], "Test session hash should allow indifferent access" + assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access" + end + + def test_deprecated_xhr_with_session + assert_deprecated { xhr :get, :set_session } + + assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key" + assert_equal 'A wonder', session[:string], "Test session hash should allow indifferent access" + assert_equal 'it works', session['symbol'], "Test session hash should allow indifferent access" + assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access" + end + def test_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] + + post :no_op + assert @request.params[:foo].blank? + end + def test_params_reset_between_post_requests - post :no_op, :foo => "bar" + post :no_op, params: { foo: "bar" } assert_equal "bar", @request.params[:foo] post :no_op @@ -574,15 +743,15 @@ XML end def test_filtered_parameters_reset_between_requests - get :no_op, :foo => "bar" + get :no_op, params: { foo: "bar" } assert_equal "bar", @request.filtered_parameters[:foo] - get :no_op, :foo => "baz" + get :no_op, params: { foo: "baz" } assert_equal "baz", @request.filtered_parameters[:foo] end def test_path_params_reset_between_request - get :test_params, :id => "foo" + get :test_params, params: { id: "foo" } assert_equal "foo", @request.path_parameters[:id] get :test_params @@ -603,19 +772,38 @@ XML end def test_request_format - get :test_format, :format => 'html' + get :test_format, params: { format: 'html' } + assert_equal 'text/html', @response.body + + get :test_format, params: { format: 'json' } + assert_equal 'application/json', @response.body + + get :test_format, params: { format: 'xml' } + assert_equal 'application/xml', @response.body + + get :test_format assert_equal 'text/html', @response.body + end - get :test_format, :format => 'json' + def test_request_format_kwarg + get :test_format, format: 'html' + assert_equal 'text/html', @response.body + + get :test_format, format: 'json' assert_equal 'application/json', @response.body - get :test_format, :format => 'xml' + get :test_format, format: 'xml' assert_equal 'application/xml', @response.body get :test_format assert_equal 'text/html', @response.body end + def test_request_format_kwarg_overrides_params + get :test_format, format: 'json', params: { format: 'html' } + assert_equal 'application/json', @response.body + end + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set cookies['foo'] = 'bar' get :no_op @@ -699,8 +887,16 @@ XML assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read end + def test_fixture_file_upload_should_be_able_access_to_tempfile + file = fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + assert file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing" + end + def test_fixture_file_upload - post :test_file_upload, :file => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + post :test_file_upload, + params: { + file: fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + } assert_equal '159528', @response.body end @@ -716,10 +912,21 @@ XML assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read end + def test_deprecated_action_dispatch_uploaded_file_upload + filename = 'mona_lisa.jpg' + path = "#{FILES_DIR}/#{filename}" + assert_deprecated { + post :test_file_upload, file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + } + assert_equal '159528', @response.body + end + def test_action_dispatch_uploaded_file_upload filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" - post :test_file_upload, :file => ActionDispatch::Http::UploadedFile.new(:filename => path, :type => "image/jpg", :tempfile => File.open(path)) + post :test_file_upload, params: { + file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + } assert_equal '159528', @response.body end @@ -742,6 +949,52 @@ XML end end +class ResponseDefaultHeadersTest < ActionController::TestCase + class TestController < ActionController::Base + def remove_header + headers.delete params[:header] + head :ok, 'C' => '3' + end + end + + setup do + @original = ActionDispatch::Response.default_headers + @defaults = { 'A' => '1', 'B' => '2' } + ActionDispatch::Response.default_headers = @defaults + end + + teardown do + ActionDispatch::Response.default_headers = @original + end + + 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 + get ':controller(/:action(/:id))' + end + end + end + + test "response contains default headers" do + # Response headers start out with the defaults + assert_equal @defaults, response.headers + + get :remove_header, params: { header: 'A' } + assert_response :ok + + # After a request, the response in the test case doesn't have the + # defaults merged on top again. + assert_not_includes response.headers, 'A' + assert_includes response.headers, 'B' + assert_includes response.headers, 'C' + end +end + module EngineControllerTests class Engine < ::Rails::Engine isolate_namespace EngineControllerTests @@ -753,7 +1006,7 @@ module EngineControllerTests class BarController < ActionController::Base def index - render :text => 'bar' + render text: 'bar' end end @@ -830,7 +1083,7 @@ class NamedRoutesControllerTest < ActionController::TestCase with_routing do |set| set.draw { resources :contents } assert_equal 'http://test.host/contents/new', new_content_url - assert_equal 'http://test.host/contents/1', content_url(:id => 1) + assert_equal 'http://test.host/contents/1', content_url(id: 1) end end end @@ -839,7 +1092,7 @@ class AnonymousControllerTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def index - render :text => params[:controller] + render text: params[:controller] end end.new @@ -860,29 +1113,29 @@ class RoutingDefaultsTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def post - render :text => request.fullpath + render text: request.fullpath end def project - render :text => request.fullpath + render text: request.fullpath end end.new @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do - get '/posts/:id', :to => 'anonymous#post', :bucket_type => 'post' - get '/projects/:id', :to => 'anonymous#project', :defaults => { :bucket_type => 'project' } + get '/posts/:id', to: 'anonymous#post', bucket_type: 'post' + get '/projects/:id', to: 'anonymous#project', defaults: { bucket_type: 'project' } end end end def test_route_option_can_be_passed_via_process - get :post, :id => 1, :bucket_type => 'post' + get :post, params: { id: 1, bucket_type: 'post'} assert_equal '/posts/1', @response.body end def test_route_default_is_not_required_for_building_request_uri - get :project, :id => 2 + get :project, params: { id: 2 } assert_equal '/projects/2', @response.body end end diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index 24a09222b1..0e4c2b7c32 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/core_ext/object/with_options' diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 0ffa2d2a03..31677f202d 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -30,8 +30,8 @@ module AbstractController assert_equal '/foo/zot', path end - def add_host! - W.default_url_options[:host] = 'www.basecamphq.com' + def add_host!(app = W) + app.default_url_options[:host] = 'www.basecamphq.com' end def add_port! @@ -255,6 +255,20 @@ module AbstractController ) end + def test_relative_url_root_is_respected_with_environment_variable + # `config.relative_url_root` is set by ENV['RAILS_RELATIVE_URL_ROOT'] + w = Class.new { + config = ActionDispatch::Routing::RouteSet::Config.new '/subdir' + r = ActionDispatch::Routing::RouteSet.new(config) + r.draw { get ':controller(/:action(/:id(.:format)))' } + include r.url_helpers + } + add_host!(w) + assert_equal('https://www.basecamphq.com/subdir/c/a/i', + w.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') + ) + end + def test_named_routes with_routing do |set| set.draw do diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 2b109ff19e..21fa670bb6 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -35,7 +35,9 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_post_json with_test_route_set do - post "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json' + post "/", + params: '{"entry":{"summary":"content..."}}', + headers: { 'CONTENT_TYPE' => 'application/json' } assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) @@ -45,7 +47,9 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_put_json with_test_route_set do - put "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json' + put "/", + params: '{"entry":{"summary":"content..."}}', + headers: { 'CONTENT_TYPE' => 'application/json' } assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) @@ -56,8 +60,9 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_register_and_use_json_simple with_test_route_set do with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do - post "/", '{"request":{"summary":"content...","title":"JSON"}}', - 'CONTENT_TYPE' => 'application/json' + post "/", + params: '{"request":{"summary":"content...","title":"JSON"}}', + headers: { 'CONTENT_TYPE' => 'application/json' } assert_equal 'summary, title', @controller.response.body assert @controller.params.has_key?(:summary) @@ -70,14 +75,16 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_use_json_with_empty_request with_test_route_set do - assert_nothing_raised { post "/", "", 'CONTENT_TYPE' => 'application/json' } + assert_nothing_raised { post "/", headers: { 'CONTENT_TYPE' => 'application/json' } } assert_equal '', @controller.response.body end end def test_dasherized_keys_as_json with_test_route_set do - post "/?full=1", '{"first-key":{"sub-key":"..."}}', 'CONTENT_TYPE' => 'application/json' + post "/?full=1", + params: '{"first-key":{"sub-key":"..."}}', + headers: { 'CONTENT_TYPE' => 'application/json' } assert_equal 'action, controller, first-key(sub-key), full', @controller.response.body assert_equal "...", @controller.params['first-key']['sub-key'] end @@ -87,7 +94,9 @@ class WebServiceTest < ActionDispatch::IntegrationTest with_test_route_set do with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do assert_raises(Interrupt) do - post "/", '{"title":"JSON"}}', 'CONTENT_TYPE' => 'application/json' + post "/", + params: '{"title":"JSON"}}', + headers: { 'CONTENT_TYPE' => 'application/json' } end end end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 1e5ed60b09..a867aee7ec 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -86,21 +86,21 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test 'skip diagnosis if not showing detailed exceptions' do @app = ProductionApp assert_raise RuntimeError do - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } end end test 'skip diagnosis if not showing exceptions' do @app = DevelopmentApp assert_raise RuntimeError do - get "/", {}, {'action_dispatch.show_exceptions' => false} + get "/", headers: { 'action_dispatch.show_exceptions' => false } end end test 'raise an exception on cascade pass' do @app = ProductionApp assert_raise ActionController::RoutingError do - get "/pass", {}, {'action_dispatch.show_exceptions' => true} + get "/pass", headers: { 'action_dispatch.show_exceptions' => true } end end @@ -108,14 +108,14 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest boomer = Boomer.new(false) @app = ActionDispatch::DebugExceptions.new(boomer) assert_raise ActionController::RoutingError do - get "/pass", {}, {'action_dispatch.show_exceptions' => true} + get "/pass", headers: { 'action_dispatch.show_exceptions' => true } end assert boomer.closed, "Expected to close the response body" end test 'displays routes in a table when a RoutingError occurs' do @app = DevelopmentApp - get "/pass", {}, {'action_dispatch.show_exceptions' => true} + get "/pass", headers: { 'action_dispatch.show_exceptions' => true } routing_table = body[/route_table.*<.table>/m] assert_match '/:controller(/:action)(.:format)', routing_table assert_match ':controller#:action', routing_table @@ -125,7 +125,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test 'displays request and response info when a RoutingError occurs' do @app = DevelopmentApp - get "/pass", {}, {'action_dispatch.show_exceptions' => true} + get "/pass", headers: { 'action_dispatch.show_exceptions' => true } assert_select 'h2', /Request/ assert_select 'h2', /Response/ @@ -134,27 +134,27 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "rescue with diagnostics message" do @app = DevelopmentApp - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_match(/puke/, body) - get "/not_found", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_match(/#{AbstractController::ActionNotFound.name}/, body) - get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_match(/ActionController::MethodNotAllowed/, body) - get "/unknown_http_method", {}, {'action_dispatch.show_exceptions' => true} + get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_match(/ActionController::UnknownHttpMethod/, body) - get "/bad_request", {}, {'action_dispatch.show_exceptions' => true} + get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true } assert_response 400 assert_match(/ActionController::BadRequest/, body) - get "/parameter_missing", {}, {'action_dispatch.show_exceptions' => true} + get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true } assert_response 400 assert_match(/ActionController::ParameterMissing/, body) end @@ -163,38 +163,38 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest @app = DevelopmentApp xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'} - get "/", {}, xhr_request_env + get "/", headers: xhr_request_env assert_response 500 assert_no_match(/<header>/, body) assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type assert_match(/RuntimeError\npuke/, body) - get "/not_found", {}, xhr_request_env + get "/not_found", headers: xhr_request_env assert_response 404 assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type assert_match(/#{AbstractController::ActionNotFound.name}/, body) - get "/method_not_allowed", {}, xhr_request_env + get "/method_not_allowed", headers: xhr_request_env assert_response 405 assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::MethodNotAllowed/, body) - get "/unknown_http_method", {}, xhr_request_env + get "/unknown_http_method", headers: xhr_request_env assert_response 405 assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::UnknownHttpMethod/, body) - get "/bad_request", {}, xhr_request_env + get "/bad_request", headers: xhr_request_env assert_response 400 assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::BadRequest/, body) - get "/parameter_missing", {}, xhr_request_env + get "/parameter_missing", headers: xhr_request_env assert_response 400 assert_no_match(/<body>/, body) assert_equal "text/plain", response.content_type @@ -204,8 +204,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "does not show filtered parameters" do @app = DevelopmentApp - get "/", {"foo"=>"bar"}, {'action_dispatch.show_exceptions' => true, - 'action_dispatch.parameter_filter' => [:foo]} + get "/", params: { "foo"=>"bar" }, headers: { 'action_dispatch.show_exceptions' => true, + 'action_dispatch.parameter_filter' => [:foo] } assert_response 500 assert_match(""foo"=>"[FILTERED]"", body) end @@ -213,7 +213,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "show registered original exception for wrapped exceptions" do @app = DevelopmentApp - get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_match(/AbstractController::ActionNotFound/, body) end @@ -221,7 +221,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "named urls missing keys raise 500 level error" do @app = DevelopmentApp - get "/missing_keys", {}, {'action_dispatch.show_exceptions' => true} + get "/missing_keys", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_match(/ActionController::UrlGenerationError/, body) @@ -229,7 +229,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "show the controller name in the diagnostics template when controller name is present" do @app = DevelopmentApp - get("/runtime_error", {}, { + get("/runtime_error", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.request.parameters' => { 'action' => 'show', @@ -252,7 +252,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest } } - get("/runtime_error", {}, { + get("/runtime_error", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.request.parameters' => { 'action' => 'show', @@ -267,21 +267,21 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test "sets the HTTP charset parameter" do @app = DevelopmentApp - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] end test 'uses logger from env' do @app = DevelopmentApp output = StringIO.new - get "/", {}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output)} + get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output) } assert_match(/puke/, output.rewind && output.read) end test 'uses backtrace cleaner from env' do @app = DevelopmentApp cleaner = stub(:clean => ['passed backtrace cleaner']) - get "/", {}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner} + get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner } assert_match(/passed backtrace cleaner/, body) end @@ -294,25 +294,25 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest 'action_dispatch.logger' => Logger.new(output), 'action_dispatch.backtrace_cleaner' => backtrace_cleaner} - get "/", {}, env + get "/", headers: env assert_operator((output.rewind && output.read).lines.count, :>, 10) end test 'display backtrace when error type is SyntaxError' do @app = DevelopmentApp - get '/original_syntax_error', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new} + get '/original_syntax_error', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new } assert_response 500 assert_select '#Application-Trace' do - assert_select 'pre code', /\(eval\):1: syntax error, unexpected/ + assert_select 'pre code', /syntax error, unexpected/ end end test 'display backtrace on template missing errors' do @app = DevelopmentApp - get "/missing_template", nil, {} + get "/missing_template" assert_select "header h1", /Template is missing/ @@ -328,11 +328,11 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test 'display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error' do @app = DevelopmentApp - get '/syntax_error_into_view', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new} + get '/syntax_error_into_view', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new } assert_response 500 assert_select '#Application-Trace' do - assert_select 'pre code', /\(eval\):1: syntax error, unexpected/ + assert_select 'pre code', /syntax error, unexpected/ end end @@ -344,7 +344,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } end - get '/framework_raises', {}, {'action_dispatch.backtrace_cleaner' => cleaner} + get '/framework_raises', headers: { 'action_dispatch.backtrace_cleaner' => cleaner } # Assert correct error assert_response 500 diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index d7408164ba..7a29a7ff97 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -34,6 +34,23 @@ module ActionDispatch assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts end + 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.expects(:source_fragment).with('c:/path/to/rails/app/controller.rb', 27).returns('nothing') + + assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts + end + + test '#source_extracts works with non standard backtrace' do + exc = TestError.new('invalid') + + wrapper = ExceptionWrapper.new({}, exc) + wrapper.expects(:source_fragment).with('invalid', 0).returns('nothing') + + assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts + end test '#application_trace returns traces only from the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index d5a4d8ee11..6a439be2b5 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -64,7 +64,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest end def test_mounting_works_with_nested_script_name - get "/foo/sprockets/omg", {}, 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/sprockets/omg' + get "/foo/sprockets/omg", headers: { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/sprockets/omg' } assert_equal "/foo/sprockets -- /omg", response.body end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index b765a13fa1..d77341bc64 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -56,7 +56,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest with_test_routing do output = StringIO.new json = "[\"person]\": {\"name\": \"David\"}}" - post "/parse", json, {'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output)} + post "/parse", params: json, headers: { 'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output) } assert_response :bad_request output.rewind && err = output.read assert err =~ /Error occurred while parsing request parameters/ @@ -79,7 +79,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest test 'raw_post is not empty for JSON request' do with_test_routing do - post '/parse', '{"posts": [{"title": "Post Title"}]}', 'CONTENT_TYPE' => 'application/json' + post '/parse', params: '{"posts": [{"title": "Post Title"}]}', headers: { 'CONTENT_TYPE' => 'application/json' } assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post end end @@ -87,7 +87,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest private def assert_parses(expected, actual, headers = {}) with_test_routing do - post "/parse", actual, headers + post "/parse", params: actual, headers: headers assert_response :ok assert_equal(expected, TestController.last_request_parameters) end @@ -146,7 +146,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest private def assert_parses(expected, actual, headers = {}) with_test_routing(UsersController) do - post "/parse", actual, headers + post "/parse", params: actual, headers: headers assert_response :ok assert_equal(expected, UsersController.last_request_parameters) assert_equal(expected.merge({"action" => "parse"}), UsersController.last_parameters) diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 926472163e..50f69c53cb 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' class MultipartParamsParsingTest < ActionDispatch::IntegrationTest @@ -37,7 +36,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end test "parse single utf8 parameter" do - assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => 'Iñtërnâtiônàlizætiøn_value'}, + assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => 'Iñtërnâtiônàlizætiøn_value'}, parse_multipart('single_utf8_param'), "request.request_parameters") assert_equal( 'Iñtërnâtiônàlizætiøn_value', @@ -45,8 +44,8 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end test "parse bracketed utf8 parameter" do - assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => { - 'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'} }, + assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => { + 'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'} }, parse_multipart('bracketed_utf8_param'), "request.request_parameters") assert_equal( {'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'}, @@ -134,13 +133,13 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest with_test_routing do fixture = FIXTURE_PATH + "/mona_lisa.jpg" params = { :uploaded_data => fixture_file_upload(fixture, "image/jpg") } - post '/read', params + post '/read', params: params end end test "uploads and reads file" do with_test_routing do - post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain") + post '/read', params: { uploaded_data: fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain") } assert_equal "File: Hello", response.body end end @@ -152,7 +151,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest get ':action', controller: 'multipart_params_parsing_test/test' end headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" } - get "/parse", {}, headers + get "/parse", headers: headers assert_response :ok end end @@ -169,7 +168,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest def parse_multipart(name) with_test_routing do headers = fixture(name) - post "/parse", headers.delete("rack.input"), headers + post "/parse", params: headers.delete("rack.input"), headers: headers assert_response :ok TestController.last_request_parameters end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index 50daafbb54..bc6716525e 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -147,7 +147,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest get ':action', :to => ::QueryStringParsingTest::TestController end - get "/parse", nil, "QUERY_STRING" => "foo[]=bar&foo[4]=bar" + get "/parse", headers: { "QUERY_STRING" => "foo[]=bar&foo[4]=bar" } assert_response :bad_request end end @@ -162,8 +162,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest middleware.use(EarlyParse) end - - get "/parse", actual + get "/parse", params: actual assert_response :ok assert_equal(expected, ::QueryStringParsingTest::TestController.last_query_parameters) end diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 1de05cbf09..365edf849a 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -131,7 +131,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest test "ambiguous params returns a bad request" do with_test_routing do - post "/parse", "foo[]=bar&foo[4]=bar" + post "/parse", params: "foo[]=bar&foo[4]=bar" assert_response :bad_request end end @@ -148,7 +148,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest def assert_parses(expected, actual) with_test_routing do - post "/parse", actual + post "/parse", params: actual assert_response :ok assert_equal expected, TestController.last_request_parameters assert_utf8 TestController.last_request_parameters diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb index a8050b4fab..00d8caf8f4 100644 --- a/actionpack/test/dispatch/request_id_test.rb +++ b/actionpack/test/dispatch/request_id_test.rb @@ -2,19 +2,23 @@ require 'abstract_unit' class RequestIdTest < ActiveSupport::TestCase test "passing on the request id from the outside" do - assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid + assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').request_id end test "ensure that only alphanumeric uurids are accepted" do - assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').uuid + assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').request_id end test "ensure that 255 char limit on the request id is being enforced" do - assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).uuid + assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).request_id end test "generating a request id when none is supplied" do - assert_match(/\w+-\w+-\w+-\w+-\w+/, stub_request.uuid) + assert_match(/\w+-\w+-\w+-\w+-\w+/, stub_request.request_id) + end + + test "uuid alias" do + assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid end private @@ -41,7 +45,7 @@ class RequestIdResponseTest < ActionDispatch::IntegrationTest test "request id given on request is passed all the way to the response" do with_test_route_set do - get '/', {}, 'HTTP_X_REQUEST_ID' => 'X' * 500 + get '/', headers: { 'HTTP_X_REQUEST_ID' => 'X' * 500 } assert_equal "X" * 255, @response.headers["X-Request-Id"] end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index ee8e915610..27ee8603e4 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -435,6 +435,9 @@ class RequestHost < BaseRequestTest request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org" assert_equal "www.secondhost.org", request.host + + request = stub_request 'HTTP_X_FORWARDED_HOST' => "", 'HTTP_HOST' => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host end test "http host with default port overrides server port" do @@ -660,6 +663,7 @@ class RequestMethod < BaseRequestTest assert_equal 'GET', request.request_method assert_equal 'GET', request.env["REQUEST_METHOD"] + assert request.get? end test "invalid http method raises exception" do @@ -1125,35 +1129,47 @@ class RequestEtag < BaseRequestTest end class RequestVariant < BaseRequestTest - test "setting variant" do - request = stub_request + def setup + super + @request = stub_request + end - request.variant = :mobile - assert_equal [:mobile], request.variant + test 'setting variant to a symbol' do + @request.variant = :phone - request.variant = [:phone, :tablet] - assert_equal [:phone, :tablet], request.variant + assert @request.variant.phone? + assert_not @request.variant.tablet? + assert @request.variant.any?(:phone, :tablet) + assert_not @request.variant.any?(:tablet, :desktop) + end - assert_raise ArgumentError do - request.variant = [:phone, "tablet"] - end + test 'setting variant to an array of symbols' do + @request.variant = [:phone, :tablet] - assert_raise ArgumentError do - request.variant = "yolo" - end + assert @request.variant.phone? + assert @request.variant.tablet? + assert_not @request.variant.desktop? + assert @request.variant.any?(:tablet, :desktop) + assert_not @request.variant.any?(:desktop, :watch) end - test "reset variant" do - request = stub_request + test 'clearing variant' do + @request.variant = nil - request.variant = nil - assert_equal nil, request.variant + assert @request.variant.empty? + assert_not @request.variant.phone? + assert_not @request.variant.any?(:phone, :tablet) end - test "setting variant with non symbol value" do - request = stub_request + test 'setting variant to a non-symbol value' do + assert_raise ArgumentError do + @request.variant = 'phone' + end + end + + test 'setting variant to an array containing a non-symbol value' do assert_raise ArgumentError do - request.variant = "mobile" + @request.variant = [:phone, 'tablet'] end end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index c61423dce4..5fbd19acdf 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -254,10 +254,6 @@ class ResponseTest < ActiveSupport::TestCase end class ResponseIntegrationTest < ActionDispatch::IntegrationTest - def app - @app - end - test "response cache control from railsish app" do @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 3d3d4b74ae..4047214843 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -26,14 +26,6 @@ module ActionDispatch inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n") end - def test_json_regexp_converter - @set.draw do - get '/cart', :to => 'cart#show' - end - route = ActionDispatch::Routing::RouteWrapper.new(@set.routes.first) - assert_equal "^\\/cart(?:\\.([^\\/.?]+))?$", route.json_regexp - end - def test_displaying_routes_for_engines engine = Class.new(Rails::Engine) do def self.inspect @@ -321,6 +313,22 @@ module ActionDispatch assert_equal ["Prefix Verb URI Pattern Controller#Action", " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output end + + def test_inspect_routes_shows_resources_route_when_assets_disabled + @set = ActionDispatch::Routing::RouteSet.new + app = ActiveSupport::OrderedOptions.new + + Rails.stubs(:application).returns(app) + + output = draw do + get '/cart', to: 'cart#show' + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " cart GET /cart(.:format) cart#show" + ], output + end end end end diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb index fe52c50336..9327fe12c6 100644 --- a/actionpack/test/dispatch/routing/route_set_test.rb +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -17,6 +17,16 @@ module ActionDispatch @set = RouteSet.new end + test "not being empty when route is added" do + assert empty? + + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_not empty? + end + test "url helpers are added when route is added" do draw do get 'foo', to: SimpleApp.new('foo#index') @@ -136,6 +146,10 @@ module ActionDispatch def url_helpers @set.url_helpers end + + def empty? + @set.empty? + end end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 450681c356..77f86b7a62 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -167,6 +167,46 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/session/reset', reset_session_path end + def test_session_singleton_resource_for_api_app + self.class.stub_controllers do |_| + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + routes = ActionDispatch::Routing::RouteSet.new(config) + + 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 @@ -362,22 +402,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor end - get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + get '/admin', headers: { 'REMOTE_ADDR' => '192.168.1.100' } assert_equal 'queenbee#index', @response.body - get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} + get '/admin', headers: { 'REMOTE_ADDR' => '10.0.0.100' } assert_equal 'pass', @response.headers['X-Cascade'] - get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + get '/admin/accounts', headers: { 'REMOTE_ADDR' => '192.168.1.100' } assert_equal 'queenbee#accounts', @response.body - get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} + get '/admin/accounts', headers: { 'REMOTE_ADDR' => '10.0.0.100' } assert_equal 'pass', @response.headers['X-Cascade'] - get '/admin/passwords', {}, {'REMOTE_ADDR' => '192.168.1.100'} + get '/admin/passwords', headers: { 'REMOTE_ADDR' => '192.168.1.100' } assert_equal 'queenbee#passwords', @response.body - get '/admin/passwords', {}, {'REMOTE_ADDR' => '10.0.0.100'} + get '/admin/passwords', headers: { 'REMOTE_ADDR' => '10.0.0.100' } assert_equal 'pass', @response.headers['X-Cascade'] end @@ -509,6 +549,41 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_for_api_app + self.class.stub_controllers do |_| + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + routes = ActionDispatch::Routing::RouteSet.new(config) + 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 @@ -1430,6 +1505,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'api/v3/products#list', @response.body end + def test_not_matching_shorthand_with_dynamic_parameters + draw do + get ':controller/:action/admin' + end + + get '/finances/overview/admin' + assert_equal 'finances#overview', @response.body + end + def test_controller_option_with_nesting_and_leading_slash draw do scope '/job', controller: 'job' do @@ -1683,9 +1767,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/products/0001/images/0001' assert_equal 'images#show', @response.body - get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} + get '/dashboard', headers: { 'REMOTE_ADDR' => '10.0.0.100' } assert_equal 'pass', @response.headers['X-Cascade'] - get '/dashboard', {}, {'REMOTE_ADDR' => '192.168.1.100'} + get '/dashboard', headers: { 'REMOTE_ADDR' => '192.168.1.100' } assert_equal 'dashboards#show', @response.body end @@ -3439,6 +3523,63 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/bar/comments/1', comment_path('1') end + def test_resource_where_as_is_empty + draw do + resource :post, as: '' + + scope 'post', as: 'post' do + resource :comment, as: '' + end + end + + assert_equal '/post/new', new_path + assert_equal '/post/comment/new', new_post_path + end + + def test_resources_where_as_is_empty + draw do + resources :posts, as: '' + + scope 'posts', as: 'posts' do + resources :comments, as: '' + end + end + + assert_equal '/posts/new', new_path + assert_equal '/posts/comments/new', new_posts_path + end + + def test_scope_where_as_is_empty + draw do + scope 'post', as: '' do + resource :user + resources :comments + end + end + + assert_equal '/post/user/new', new_user_path + assert_equal '/post/comments/new', new_comment_path + end + + def test_head_fetch_with_mount_on_root + draw do + get '/home' => 'test#index' + mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/' + end + + # TODO: 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 + + # But the Rack app can still respond to its own HEAD requests. + head '/foobar' + assert_response :ok + assert_equal 'HEAD', @response.body + end + private def draw(&block) @@ -3517,7 +3658,11 @@ class TestAltApp < ActionDispatch::IntegrationTest end end - AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) + AltRoutes = Class.new(ActionDispatch::Routing::RouteSet) { + def request_class + AltRequest + end + }.new AltRoutes.draw do get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} get "/" => TestAltApp::AltApp.new @@ -3535,12 +3680,12 @@ class TestAltApp < ActionDispatch::IntegrationTest end def test_alt_request_with_matched_header - get "/", {}, "HTTP_X_HEADER" => "HEADER" + get "/", headers: { "HTTP_X_HEADER" => "HEADER" } assert_equal "XHeader", @response.body end def test_alt_request_with_unmatched_header - get "/", {}, "HTTP_X_HEADER" => "NON_MATCH" + get "/", headers: { "HTTP_X_HEADER" => "NON_MATCH" } assert_equal "Alternative App", @response.body end end @@ -3735,7 +3880,7 @@ class TestHttpMethods < ActionDispatch::IntegrationTest (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| test "request method #{method.underscore} can be matched" do - get '/', nil, 'REQUEST_METHOD' => method + get '/', headers: { 'REQUEST_METHOD' => method } assert_equal method, @response.body end end @@ -4406,6 +4551,19 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } assert_equal message, error.message end + + test "url helpers raise message with mixed parameters when generation fails " do + url, missing = { action: 'show', controller: 'products', id: nil, "id"=>"url-tested"}, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil, 'id'=>'url-tested') } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil, 'id'=>'url-tested') } + assert_equal message, error.message + end end class TestDefaultUrlOptions < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index 9f810cad01..22a46b0930 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -22,7 +22,7 @@ class CacheStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "#{request.session_options[:id]}" + render :text => "#{request.session.id}" end def call_reset_session diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index c5cd24d06e..e7f4235de8 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -29,7 +29,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "id: #{request.session_options[:id]}" + render :text => "id: #{request.session.id}" end def get_class_after_reset_session @@ -53,7 +53,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def change_session_id - request.session_options[:id] = nil + request.session.options[:id] = nil get_session_id end @@ -125,7 +125,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_does_set_secure_cookies_over_https with_test_route_set(:secure => true) do - get '/set_session_value', nil, 'HTTPS' => 'on' + get '/set_session_value', headers: { 'HTTPS' => 'on' } assert_response :success assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly", headers['Set-Cookie'] @@ -331,9 +331,11 @@ class CookieStoreTest < ActionDispatch::IntegrationTest private # Overwrite get to send SessionSecret in env hash - def get(path, parameters = nil, env = {}) - env["action_dispatch.key_generator"] ||= Generator - super + def get(path, *args) + args[0] ||= {} + args[0][:headers] ||= {} + args[0][:headers]["action_dispatch.key_generator"] ||= Generator + super(path, *args) end def with_test_route_set(options = {}) diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index f7a06cfed4..9a5d5131c0 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -23,7 +23,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "#{request.session_options[:id]}" + render :text => "#{request.session.id}" end def call_reset_session @@ -172,7 +172,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest reset! - get '/set_session_value', :_session_id => session_id + get '/set_session_value', params: { _session_id: session_id } assert_response :success assert_not_equal session_id, cookies['_session_id'] end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 323fbc285e..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" @@ -27,30 +27,30 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest test "skip exceptions app if not showing exceptions" do @app = ProductionApp assert_raise RuntimeError do - get "/", {}, {'action_dispatch.show_exceptions' => false} + get "/", headers: { 'action_dispatch.show_exceptions' => false } end end test "rescue with error page" do @app = ProductionApp - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_equal "500 error fixture\n", body - get "/bad_params", {}, {'action_dispatch.show_exceptions' => true} + get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true } assert_response 400 assert_equal "400 error fixture\n", body - get "/not_found", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_equal "404 error fixture\n", body - get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_equal "", body - get "/unknown_http_method", {}, {'action_dispatch.show_exceptions' => true} + get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_equal "", body end @@ -61,11 +61,11 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest begin @app = ProductionApp - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_equal "500 localized error fixture\n", body - get "/not_found", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_equal "404 error fixture\n", body ensure @@ -76,14 +76,14 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest test "sets the HTTP charset parameter" do @app = ProductionApp - get "/", {}, {'action_dispatch.show_exceptions' => true} + get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] end test "show registered original exception for wrapped exceptions" do @app = ProductionApp - get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_match(/404 error/, body) end @@ -97,7 +97,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest end @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) - get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_equal "YOU FAILED BRO", body end @@ -108,7 +108,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest end @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) - get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_equal "", body end diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index c3598c5e8e..7ced41bc2e 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -20,7 +20,7 @@ class SSLTest < ActionDispatch::IntegrationTest end def test_allows_https_proxy_header_url - get "http://example.org/", {}, 'HTTP_X_FORWARDED_PROTO' => "https" + get "http://example.org/", headers: { 'HTTP_X_FORWARDED_PROTO' => "https" } assert_response :success end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 7f1207eaed..95971b3a0e 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -1,9 +1,21 @@ -# encoding: utf-8 require 'abstract_unit' -require 'rbconfig' require 'zlib' module StaticTests + def setup + silence_warnings do + @default_internal_encoding = Encoding.default_internal + @default_external_encoding = Encoding.default_external + end + end + + def teardown + silence_warnings do + Encoding.default_internal = @default_internal_encoding + Encoding.default_external = @default_external_encoding + end + end + def test_serves_dynamic_content assert_equal "Hello, World!", get("/nofile").body end @@ -12,6 +24,18 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%E3E4").body end + def test_handles_urls_with_ascii_8bit + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + + def test_handles_urls_with_ascii_8bit_on_win_31j + silence_warnings do + Encoding.default_internal = "Windows-31J" + Encoding.default_external = "Windows-31J" + end + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + def test_sets_cache_control response = get("/index.html") assert_html "/index.html", response @@ -33,6 +57,7 @@ module StaticTests def test_serves_static_index_file_in_directory assert_html "/foo/index.html", get("/foo/index.html") + assert_html "/foo/index.html", get("/foo/index") assert_html "/foo/index.html", get("/foo/") assert_html "/foo/index.html", get("/foo") end @@ -145,6 +170,16 @@ module StaticTests assert_equal default_response.headers['Content-Type'], response.headers['Content-Type'] end + def test_serves_gzip_files_with_not_modified + file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" + last_modified = File.mtime(File.join(@root, "#{file_name}.gz")) + response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip', 'HTTP_IF_MODIFIED_SINCE' => last_modified.httpdate) + assert_equal 304, response.status + assert_equal nil, response.headers['Content-Type'] + assert_equal nil, response.headers['Content-Encoding'] + assert_equal nil, response.headers['Vary'] + end + # Windows doesn't allow \ / : * ? " < > | in filenames unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ def test_serves_static_file_with_colon @@ -200,6 +235,7 @@ class StaticTest < ActiveSupport::TestCase } def setup + super @root = "#{FIXTURE_LOAD_PATH}/public" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end @@ -225,10 +261,24 @@ class StaticTest < ActiveSupport::TestCase } assert_equal(DummyApp.call(nil), @app.call(env)) end + + def test_non_default_static_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("/") + assert_html "/other-index.html", get("") + assert_html "/foo/other-index.html", get("/foo/other-index.html") + assert_html "/foo/other-index.html", get("/foo/other-index") + assert_html "/foo/other-index.html", get("/foo/") + assert_html "/foo/other-index.html", get("/foo") + end + end class StaticEncodingTest < StaticTest def setup + super @root = "#{FIXTURE_LOAD_PATH}/公共" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end 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/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index 8f79e7bf9a..ce1e1d0a6a 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -39,12 +39,12 @@ module TestUrlGeneration end test "the request's SCRIPT_NAME takes precedence over the route" do - get "/foo", {}, 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes + get "/foo", headers: { 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes } assert_equal "/new/foo", response.body end test "the request's SCRIPT_NAME wraps the mounted app's" do - get '/new/bar/foo', {}, 'SCRIPT_NAME' => '/new', 'PATH_INFO' => '/bar/foo', 'action_dispatch.routes' => Routes + get '/new/bar/foo', headers: { 'SCRIPT_NAME' => '/new', 'PATH_INFO' => '/bar/foo', 'action_dispatch.routes' => Routes } assert_equal "/new/bar/foo", response.body end diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb new file mode 100644 index 0000000000..521b1450df --- /dev/null +++ b/actionpack/test/fixtures/collection_cache/index.html.erb @@ -0,0 +1 @@ +<%= render @customers %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/customers/_commented_customer.html.erb b/actionpack/test/fixtures/customers/_commented_customer.html.erb new file mode 100644 index 0000000000..8cc9c1ec13 --- /dev/null +++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb @@ -0,0 +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 new file mode 100644 index 0000000000..5105090d4b --- /dev/null +++ b/actionpack/test/fixtures/customers/_customer.html.erb @@ -0,0 +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/public/foo/other-index.html b/actionpack/test/fixtures/public/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/public/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/other-index.html b/actionpack/test/fixtures/public/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/public/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb b/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb deleted file mode 100644 index bda57d0fae..0000000000 --- a/actionpack/test/fixtures/symlink_parent/symlinked_layout.erb +++ /dev/null @@ -1,5 +0,0 @@ -This is my layout - -<%= yield %> - -End. diff --git a/actionpack/test/fixtures/公共/foo/other-index.html b/actionpack/test/fixtures/公共/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/other-index.html b/actionpack/test/fixtures/公共/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/公共/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index 21d867aca0..eff96a0abc 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.new("name", app, path, {}, [], defaults) assert_equal app, route.app assert_equal path, route.path @@ -18,30 +18,38 @@ module ActionDispatch app = Object.new path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} - route = Route.new("name", app, path, {}, defaults) + route = Route.new("name", app, path, {}, [], defaults) route.ast.grep(Nodes::Terminal).each do |node| assert_equal route, node.memo end end + def test_path_requirements_override_defaults + strexp = Router::Strexp.build(':name', { name: /love/ }, ['/']) + path = Path::Pattern.new strexp + defaults = { name: 'tender' } + route = Route.new('name', nil, path, nil, [], 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.new("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.new("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.new("name", nil, path, {}, [], { :controller => 'foo', :action => 'bar' }) assert_equal '/foo/himom', route.format({ :controller => 'foo', @@ -51,7 +59,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.new("name", nil, path, {:action => 'bar'}, [], { :controller => 'foo' }) assert_equal '/foo/bar/10', route.format({ :controller => 'foo', @@ -62,34 +70,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.new("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.new("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.new("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.new "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.new "name", nil, path, constraints, [], {} knowledge = {:id=>20, :controller=>"pages", :action=>"show"} diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 9b2b85ec73..2b505f081e 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -1,4 +1,3 @@ -# coding: utf-8 require 'abstract_unit' module ActionDispatch diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 19c61b5914..802fb93c69 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -35,7 +35,7 @@ module ActionDispatch exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?'] path = Path::Pattern.new exp - routes.add_route nil, path, {}, {:id => nil}, {} + routes.add_route nil, path, {}, [], {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false @@ -52,7 +52,7 @@ module ActionDispatch exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?'] path = Path::Pattern.new exp - routes.add_route nil, path, {}, {:id => nil}, {} + routes.add_route nil, path, {}, [], {:id => nil}, {} env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false @@ -71,7 +71,7 @@ module ActionDispatch exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp - routes.add_route nil, path, requirements, {:id => nil}, {} + routes.add_route nil, path, requirements, [], {:id => nil}, {} env = rails_env({'PATH_INFO' => '/foo/10'}, klass) router.recognize(env) do |r, params| @@ -91,7 +91,7 @@ module ActionDispatch exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp - router.routes.add_route nil, path, requirements, {:id => nil}, {} + router.routes.add_route nil, path, requirements, [], {:id => nil}, {} env = rails_env({'PATH_INFO' => '/foo/10'}, klass) router.recognize(env) do |r, params| @@ -118,7 +118,7 @@ module ActionDispatch exp = Router::Strexp.build '/bar', {}, ['/.?'] path = Path::Pattern.new exp - routes.add_route nil, path, {}, {}, {} + routes.add_route nil, path, {}, [], {}, {} env = rails_env({'PATH_INFO' => '/foo', 'custom.path_info' => '/bar'}, CustomPathRequest) @@ -192,7 +192,7 @@ module ActionDispatch 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 + @router.routes.add_route nil, path, {}, [], {}, route_name error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate(route_name, { }, { }) @@ -234,7 +234,7 @@ module ActionDispatch def test_defaults_merge_correctly path = Path::Pattern.from_string '/foo(/:id)' - @router.routes.add_route nil, path, {}, {:id => nil}, {} + @router.routes.add_route nil, path, {}, [], {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' @router.recognize(env) do |r, params| @@ -317,7 +317,7 @@ module ActionDispatch def test_nil_path_parts_are_ignored path = Path::Pattern.from_string "/:controller(/:action(.:format))" - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } @@ -332,7 +332,7 @@ module ActionDispatch str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true) path = Path::Pattern.new str - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path @@ -340,7 +340,7 @@ module ActionDispatch def test_generate_calls_param_proc path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} parameterized = [] params = [ [:controller, "tasks"], @@ -357,7 +357,7 @@ module ActionDispatch def test_generate_id path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, params = @formatter.generate( nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) @@ -367,7 +367,7 @@ module ActionDispatch def test_generate_escapes path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, _ = @formatter.generate(nil, { :controller => "tasks", @@ -378,7 +378,7 @@ module ActionDispatch def test_generate_escapes_with_namespaced_controller path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, _ = @formatter.generate( nil, { :controller => "admin/tasks", @@ -389,7 +389,7 @@ module ActionDispatch def test_generate_extra_params path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, params = @formatter.generate( nil, { :id => 1, @@ -401,9 +401,36 @@ module ActionDispatch assert_equal({:id => 1, :relative_url_root => nil}, params) 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, {}, [], {}, {} + primarty_parameters = { + :id => 1, + :controller => "tasks", + :action => "show", + :relative_url_root => nil + } + redirection_parameters = { + 'action'=>'show', + } + missing_key = 'name' + missing_parameters ={ + missing_key => "task_1" + } + request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + + message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate( + nil, request_parameters, request_parameters) + end + assert_equal message, error.message + end + def test_generate_uses_recall_if_needed path = Path::Pattern.from_string '/:controller(/:action(/:id))' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, [], {}, {} path, params = @formatter.generate( nil, @@ -415,7 +442,7 @@ module ActionDispatch def test_generate_with_name path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, "tasks" + @router.routes.add_route @app, path, {}, [], {}, "tasks" path, params = @formatter.generate( "tasks", @@ -433,7 +460,7 @@ module ActionDispatch 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, {}, {}, {}) + route = @router.routes.add_route(app, path, {}, [], {}, {}) env = rails_env 'PATH_INFO' => request_path called = false @@ -455,7 +482,7 @@ module ActionDispatch define_method("test_recognize_#{name}") do path = Path::Pattern.from_string '/:segment/*splat' app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + route = @router.routes.add_route(app, path, {}, [], {}, {}) env = rails_env 'PATH_INFO' => request_path called = false @@ -478,7 +505,7 @@ module ActionDispatch ) path = Path::Pattern.new strexp app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + route = @router.routes.add_route(app, path, {}, [], {}, {}) env = rails_env 'PATH_INFO' => '/admin/users/show/10' called = false @@ -499,7 +526,7 @@ module ActionDispatch def test_recognize_literal path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new - route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) + route = @router.routes.add_route(app, path, {}, [], {:controller => 'books'}) env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } @@ -517,7 +544,7 @@ module ActionDispatch path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new conditions = { request_method: 'HEAD' } - @router.routes.add_route(app, path, conditions, {}) + @router.routes.add_route(app, path, conditions, [], {}) env = rails_env( 'PATH_INFO' => '/books/list.rss', @@ -538,7 +565,7 @@ module ActionDispatch conditions = { :request_method => 'GET' } - @router.routes.add_route(app, path, conditions, {}) + @router.routes.add_route(app, path, conditions, [], {}) env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "HEAD" @@ -555,7 +582,7 @@ module ActionDispatch path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new conditions = { request_method: 'GET' } - @router.routes.add_route(app, path, conditions, {}) + @router.routes.add_route(app, path, conditions, [], {}) env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "POST" @@ -570,7 +597,7 @@ module ActionDispatch conditions = conditions.dup conditions[:request_method] = 'POST' - post = @router.routes.add_route(app, path, conditions, {}) + post = @router.routes.add_route(app, path, conditions, [], {}) called = false @router.recognize(env) do |r, params| @@ -590,7 +617,7 @@ module ActionDispatch else path = Path::Pattern.new path end - router.routes.add_route @app, path, {}, {}, {} + router.routes.add_route @app, path, {}, [], {}, {} end end diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb index a4efc82b8c..b9dac8751c 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -3,16 +3,22 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRoutes < ActiveSupport::TestCase + setup do + @routes = Routes.new + 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}, {} + routes.add_route nil, path, requirements, [], {:id => nil}, {} + assert_not routes.empty? assert_equal 1, routes.length routes.clear + assert routes.empty? assert_equal 0, routes.length end @@ -20,9 +26,9 @@ module ActionDispatch routes = Routes.new path = Path::Pattern.from_string '/hello' - routes.add_route nil, path, {}, {}, {} + routes.add_route nil, path, {}, [], {}, {} ast = routes.ast - routes.add_route nil, path, {}, {}, {} + routes.add_route nil, path, {}, [], {}, {} assert_not_equal ast, routes.ast end @@ -30,21 +36,37 @@ module ActionDispatch routes = Routes.new path = Path::Pattern.from_string '/hello' - routes.add_route nil, path, {}, {}, {} + routes.add_route nil, path, {}, [], {}, {} sim = routes.simulator - routes.add_route nil, path, {}, {}, {} + routes.add_route nil, path, {}, [], {}, {} assert_not_equal sim, routes.simulator end + def test_partition_route + path = Path::Pattern.from_string '/hello' + + anchored_route = @routes.add_route nil, path, {}, [], {}, {} + assert_equal [anchored_route], @routes.anchored_routes + assert_equal [], @routes.custom_routes + + strexp = Router::Strexp.build( + "/hello/:who", { who: /\d/ }, ['/', '.', '?'] + ) + path = Path::Pattern.new strexp + + custom_route = @routes.add_route nil, path, {}, [], {}, {} + assert_equal [custom_route], @routes.custom_routes + assert_equal [anchored_route], @routes.anchored_routes + end + def test_first_name_wins - #def add_route app, path, conditions, defaults, name = nil 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' + routes.add_route nil, one, {}, [], {}, 'aaron' + routes.add_route nil, two, {}, [], {}, 'aaron' assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index ab3cb7eb19..7b6008d5ed 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,6 +1,108 @@ +* 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. + + *Henrik Nygren* + +* Remove `:rescue_format` option for `translate` helper since it's no longer + supported by I18n. + + *Bernard Potocki* + +* `translate` should handle `raise` flag correctly in case of both main and default + translation is missing. + + Fixes #19967 + + *Bernard Potocki* + +* Load the `default_form_builder` from the controller on initialization, which overrides + the global config if it is present. + + *Kevin McPhillips* + +* Accept lambda as `child_index` option in `fields_for` method. + + *Karol Galanciak* + +* `translate` allows `default: [[]]` again for a default value of `[]`. + + Fixes #19640. + + *Adam Prescott* + +* `translate` should accept nils as members of the `:default` + parameter without raising a translation missing error. + + Fixes #19419 + + *Justin Coyne* + +* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY` + as input when `precision: 0` is used. + + Fixes #19227. + + *Yves Senn* + +* Fixed the translation helper method to accept different default values types + besides String. + + *Ulisses Almeida* + +* Collection rendering automatically caches and fetches multiple partials. + + Collections rendered as: + + ```ruby + <%= render @notifications %> + <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %> + ``` + + will now read several partials from cache at once, if the template starts with a cache call: + + ```ruby + # notifications/_notification.html.erb + <% cache notification do %> + <%# ... %> + <% end %> + ``` + + *Kasper Timm Hansen* + +* Fixed a dependency tracker bug that caused template dependencies not + count layouts as dependencies for partials. + + *Juho Leinonen* + +* Extracted `ActionView::Helpers::RecordTagHelper` to external gem + (`record_tag_helper`) and added removal notices. + + *Todd Bealmear* + +* Allow to pass a string value to `size` option in `image_tag` and `video_tag`. + + This makes the behavior more consistent with `width` or `height` options. + + *Mehdi Lahmam* + +* Partial template name does no more have to be a valid Ruby identifier. + + There used to be a naming rule that the partial name should start with + underscore, and should be followed by any combination of letters, numbers + and underscores. + But now we can give our partials any name starting with underscore, such as + _🍔.html.erb. + + *Akira Matsuda* + * Change the default template handler from `ERB` to `Raw`. - Files without a template handler in their extension will be rended using the raw + Files without a template handler in their extension will be rendered using the raw handler instead of ERB. *Rafael Mendonça França* @@ -28,5 +130,8 @@ *Angelo Capilleri* +* Allow entries without a link tag in `AtomFeedHelper`. + + *Daniel Gomez de Souza* Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 5bb62c7562..8b1f85f748 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -9,7 +9,7 @@ used to inline short Ruby snippets inside HTML), and XML Builder. The latest version of Action View can be installed with RubyGems: - % [sudo] gem install actionview + % gem install actionview Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionview/Rakefile b/actionview/Rakefile index 1b71435948..93be50721d 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' desc "Default Task" task :default => :test @@ -18,7 +17,7 @@ namespace :test do Rake::TestTask.new(:template) do |t| t.libs << 'test' - t.test_files = Dir.glob('test/template/**/*_test.rb').sort + t.test_files = Dir.glob('test/template/**/*_test.rb') t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -45,39 +44,8 @@ namespace :test do end end -spec = eval(File.read('actionview.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - task :lines do - lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 - - FileList["lib/**/*.rb"].each do |file_name| - next if file_name =~ /vendor/ - File.open(file_name, 'r') do |f| - while line = f.gets - lines += 1 - next if line =~ /^\s*$/ - next if line =~ /^\s*#/ - codelines += 1 - end - end - puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - - total_lines += lines - total_codelines += codelines - - lines, codelines = 0, 0 - end - - puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' + files = FileList["lib/**/*.rb"] + CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 8f9194cda7..612e94021d 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Rendering framework putting the V in MVC (part of Rails).' s.description = 'Simple, battle-tested conventions and helpers for building web pages.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' - s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_development_dependency 'actionpack', version 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 1feafc1094..43124bb904 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -70,6 +70,14 @@ module ActionView #:nodoc: # Headline: <%= headline %> # First name: <%= person.first_name %> # + # The local variables passed to sub templates can be accessed as a hash using the <tt>local_assigns</tt> hash. This lets you access the + # variables as: + # + # Headline: <%= local_assigns[:headline] %> + # + # This is useful in cases where you aren't sure if the local variable has been assigned. Alternately, you could also use + # <tt>defined? headline</tt> to first check if the variable has been assigned before using it. + # # === Template caching # # By default, Rails will compile each template to a method in order to render it. When you alter a template, diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index e34bdd4a46..7a7e116dbb 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -76,6 +76,12 @@ module ActionView (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm + LAYOUT_DEPENDENCY = /\A + (?:\s*\(?\s*) # optional opening paren surrounded by spaces + (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration + (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest + /xm + def self.call(name, template) new(name, template).dependencies end @@ -106,15 +112,20 @@ module ActionView render_calls = source.split(/\brender\b/).drop(1) render_calls.each do |arguments| - arguments.scan(RENDER_ARGUMENTS) do - add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic]) - add_static_dependency(render_dependencies, Regexp.last_match[:static]) - end + add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY) + add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS) end render_dependencies.uniq end + def add_dependencies(render_dependencies, arguments, pattern) + arguments.scan(pattern) do + add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic]) + add_static_dependency(render_dependencies, Regexp.last_match[:static]) + end + end + def add_dynamic_dependency(dependencies, dependency) if dependency dependencies << "#{dependency.pluralize}/#{dependency.singularize}" diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1f103786cb..b29eb48425 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -70,7 +70,8 @@ module ActionView def dependencies DependencyTracker.find_dependencies(name, template) 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 b7fdc16a9d..e32f8e219e 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -127,7 +127,7 @@ module ActionView # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) - # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) if !(type == :rss || type == :atom) && tag_options[:type].blank? raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") @@ -207,6 +207,7 @@ module ActionView # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> def image_tag(source, options={}) options = options.symbolize_keys + check_for_image_tag_errors(options) src = options[:src] = path_to_image(source) @@ -318,12 +319,19 @@ module ActionView end def extract_dimensions(size) + size = size.to_s if size =~ %r{\A\d+x\d+\z} size.split('x') elsif size =~ %r{\A\d+\z} [size, size] end end + + def check_for_image_tag_errors(options) + if options[:size] && (options[:height] || options[:width]) + raise ArgumentError, "Cannot pass a :size option with a :height or :width option" + end + end end end end diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 29733442c1..ef4a6c98c0 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -248,6 +248,11 @@ module ActionView # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. + # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js + # def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) end @@ -270,6 +275,11 @@ module ActionView # Computes the full URL to a stylesheet asset in the public stylesheets directory. # This will use +stylesheet_path+ internally, so most of their behaviors will be the same. + # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css + # def stylesheet_url(source, options = {}) url_to_asset(source, {type: :stylesheet}.merge!(options)) end @@ -295,6 +305,11 @@ module ActionView # Computes the full URL to an image asset. # This will use +image_path+ internally, so most of their behaviors will be the same. + # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png + # def image_url(source, options = {}) url_to_asset(source, {type: :image}.merge!(options)) end @@ -316,6 +331,11 @@ module ActionView # Computes the full URL to a video asset in the public videos directory. # This will use +video_path+ internally, so most of their behaviors will be the same. + # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi + # def video_url(source, options = {}) url_to_asset(source, {type: :video}.merge!(options)) end @@ -337,6 +357,11 @@ module ActionView # Computes the full URL to an audio asset in the public audios directory. # This will use +audio_path+ internally, so most of their behaviors will be the same. + # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav + # def audio_url(source, options = {}) url_to_asset(source, {type: :audio}.merge!(options)) end @@ -357,6 +382,11 @@ module ActionView # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. + # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf + # def font_url(source, options = {}) url_to_asset(source, {type: :font}.merge!(options)) end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 227ad4cdfa..bb1cdd0f8d 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -16,7 +16,7 @@ module ActionView # end # # app/controllers/posts_controller.rb: - # class PostsController < ApplicationController::Base + # class PostsController < ApplicationController # # GET /posts.html # # GET /posts.atom # def index @@ -51,7 +51,7 @@ module ActionView # * <tt>:language</tt>: Defaults to "en-US". # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. - # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}" + # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case. # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # 2005 is used (as an "I don't care" value). @@ -174,7 +174,7 @@ module ActionView # # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists. - # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record. + # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record. # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}" # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html". def entry(record, options = {}) @@ -191,7 +191,8 @@ module ActionView type = options.fetch(:type, 'text/html') - @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record)) + url = options.fetch(:url) { @view.polymorphic_url(record) } + @xml.link(:rel => 'alternate', :type => type, :href => url) if url yield AtomBuilder.new(@xml) end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 4db8930a26..72e2aa1807 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,8 @@ 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. Here are a few examples of things that can't be derived: + # 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 # render @project.documents.where(published: true).order('created_at') @@ -97,21 +98,47 @@ module ActionView # <%# Template Dependency: todolists/todolist %> # <%= render_sortable_todolists @project.todolists %> # - # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so. + # The pattern used to match these 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 of 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 + # 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: # # <%# Helper Dependency Updated: May 6, 2012 at 6pm %> # <%= some_helper_method(person) %> # - # Now all you'll have to do is change that timestamp when the helper method changes. + # Now all you have to do is change that timestamp when the helper method changes. + # + # === Automatic Collection Caching + # + # When rendering collections such as: + # + # <%= render @notifications %> + # <%= render partial: 'notifications/notification', collection: @notifications %> + # + # If the notifications/_notification partial starts with a cache call as: + # + # <% cache notification do %> + # <%= notification.name %> + # <% end %> + # + # 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 + # collection caching. + # + # The automatic cache multi read can be turned off like so: + # + # <%= render @notifications, cache: false %> def cache(name = {}, options = nil, &block) - if controller.perform_caching + if controller.respond_to?(:perform_caching) && controller.perform_caching safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) else yield @@ -122,7 +149,7 @@ module ActionView # Cache fragments of a view if +condition+ is true # - # <%= cache_if admin?, project do %> + # <% cache_if admin?, project do %> # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> @@ -138,7 +165,7 @@ module ActionView # Cache fragments of a view unless +condition+ is true # - # <%= cache_unless admin?, project do %> + # <% cache_unless admin?, project do %> # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> @@ -161,6 +188,14 @@ module ActionView end end + # Given a key (as described in ActionController::Caching::Fragments.expire_fragment), + # returns a key suitable for use in reading, writing, or expiring a + # cached fragment. All keys are prefixed with <tt>views/</tt> and uses + # ActiveSupport::Cache.expand_cache_key for the expansion. + def fragment_cache_key(key) + ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + end + private def fragment_name_with_digest(name) #:nodoc: diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 5a3223968f..a67ba580f1 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -195,7 +195,9 @@ module ActionView def with_output_buffer(buf = nil) #:nodoc: unless buf buf = ActionView::OutputBuffer.new - buf.force_encoding(output_buffer.encoding) if output_buffer + if output_buffer && output_buffer.respond_to?(:encoding) + buf.force_encoding(output_buffer.encoding) + end end self.output_buffer, old_buffer = buf, output_buffer yield diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb index 74ef25f7c1..3569fba8c6 100644 --- a/actionview/lib/action_view/helpers/controller_helper.rb +++ b/actionview/lib/action_view/helpers/controller_helper.rb @@ -14,6 +14,7 @@ module ActionView if @_controller = controller @_request = controller.request if controller.respond_to?(:request) @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder) end end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 4b4f0ae577..394e20ff2b 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -177,6 +177,8 @@ module ActionView # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example. # See <tt>Kernel.sprintf</tt> for documentation on format sequences. # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing). + # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is "" (i.e. nothing). + # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing). # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to # the current selected year minus 5. @@ -486,7 +488,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) diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb index ba47eee9ba..e9dccbad1c 100644 --- a/actionview/lib/action_view/helpers/debug_helper.rb +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -26,7 +26,7 @@ module ActionView Marshal::dump(object) object = ERB::Util.html_escape(object.to_yaml) content_tag(:pre, object, :class => "debug_dump") - rescue Exception # errors from Marshal or YAML + rescue # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback content_tag(:code, object.inspect, :class => "debug_dump") end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 8d78ba13d5..3a9acafaa2 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -4,6 +4,7 @@ require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' require 'action_view/helpers/active_model_helper' require 'action_view/model_naming' +require 'action_view/record_identifier' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/output_safety' @@ -66,9 +67,10 @@ module ActionView # # In particular, thanks to the conventions followed in the generated field names, the # controller gets a nested hash <tt>params[:person]</tt> with the person attributes - # set in the form. That hash is ready to be passed to <tt>Person.create</tt>: + # set in the form. That hash is ready to be passed to <tt>Person.new</tt>: # - # if @person = Person.create(params[:person]) + # @person = Person.new(params[:person]) + # if @person.save # # success # else # # error handling @@ -110,6 +112,9 @@ module ActionView include FormTagHelper include UrlHelper include ModelNaming + include RecordIdentifier + + attr_internal :default_form_builder # Creates a form that allows the user to create or update the attributes # of a specific model object. @@ -138,6 +143,7 @@ module ActionView # will get expanded to # # <%= text_field :person, :first_name %> + # # which results in an HTML <tt><input></tt> tag whose +name+ attribute is # <tt>person[first_name]</tt>. This means that when the form is submitted, # the value entered by the user will be available in the controller as @@ -1224,12 +1230,12 @@ module ActionView object_name = model_name_from_record_or_class(object).param_key end - builder = options[:builder] || default_form_builder + builder = options[:builder] || default_form_builder_class builder.new(object_name, object, self, options) end - def default_form_builder - builder = ActionView::Base.default_form_builder + def default_form_builder_class + builder = default_form_builder || ActionView::Base.default_form_builder builder.respond_to?(:constantize) ? builder.constantize : builder end end @@ -1244,7 +1250,7 @@ module ActionView # Admin: <%= person_form.check_box :admin %> # <% end %> # - # In the above block, the a +FormBuilder+ object is yielded as the + # In the above block, a +FormBuilder+ object is yielded as the # +person_form+ variable. This allows you to generate the +text_field+ # and +check_box+ fields by specifying their eponymous methods, which # modify the underlying template and associates the +@person+ model object @@ -1265,6 +1271,7 @@ module ActionView # ) # ) # end + # end # # The above code creates a new method +div_radio_button+ which wraps a div # around the new radio button. Note that when options are passed in, you @@ -1624,7 +1631,7 @@ module ActionView # target labels for radio_button tags (where the value is used in the ID of the input tag). # # ==== Examples - # label(:post, :title) + # label(:title) # # => <label for="post_title">Title</label> # # You can localize your labels based on model and attribute names. @@ -1637,7 +1644,7 @@ module ActionView # # Which then will result in # - # label(:post, :body) + # label(:body) # # => <label for="post_body">Write your entire text here</label> # # Localization can also be based purely on the translation of the attribute-name @@ -1648,21 +1655,22 @@ module ActionView # post: # cost: "Total cost" # - # label(:post, :cost) + # label(:cost) # # => <label for="post_cost">Total cost</label> # - # label(:post, :title, "A short title") + # label(:title, "A short title") # # => <label for="post_title">A short title</label> # - # label(:post, :title, "A short title", class: "title_label") + # label(:title, "A short title", class: "title_label") # # => <label for="post_title" class="title_label">A short title</label> # - # label(:post, :privacy, "Public Post", value: "public") + # label(:privacy, "Public Post", value: "public") # # => <label for="post_privacy_public">Public Post</label> # - # label(:post, :terms) do + # label(:terms) do # 'Accept <a href="/terms">Terms</a>.'.html_safe # end + # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label> def label(method, text = nil, options = {}, &block) @template.label(@object_name, method, text, objectify_options(options), &block) end @@ -1711,16 +1719,17 @@ module ActionView # hashes instead of arrays. # # # Let's say that @post.validated? is 1: - # check_box("post", "validated") + # check_box("validated") # # => <input name="post[validated]" type="hidden" value="0" /> # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" /> # # # Let's say that @puppy.gooddog is "no": - # check_box("puppy", "gooddog", {}, "yes", "no") + # check_box("gooddog", {}, "yes", "no") # # => <input name="puppy[gooddog]" type="hidden" value="no" /> # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> # - # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no") + # # Let's say that @eula.accepted is "no": + # check_box("accepted", { class: 'eula_check' }, "yes", "no") # # => <input name="eula[accepted]" type="hidden" value="no" /> # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") @@ -1735,13 +1744,14 @@ module ActionView # +options+ hash. You may pass HTML options there as well. # # # Let's say that @post.category returns "rails": - # radio_button("post", "category", "rails") - # radio_button("post", "category", "java") + # radio_button("category", "rails") + # radio_button("category", "java") # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # - # radio_button("user", "receive_newsletter", "yes") - # radio_button("user", "receive_newsletter", "no") + # # Let's say that @user.category returns "no": + # radio_button("receive_newsletter", "yes") + # radio_button("receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" /> def radio_button(method, tag_value, options = {}) @@ -1754,14 +1764,17 @@ module ActionView # shown. # # ==== Examples - # hidden_field(:signup, :pass_confirm) - # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # # Let's say that @signup.pass_confirm returns true: + # hidden_field(:pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" /> # - # hidden_field(:post, :tag_list) - # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # # Let's say that @post.tag_list returns "blog, ruby": + # hidden_field(:tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" /> # - # hidden_field(:user, :token) - # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + # # Let's say that @user.token returns "abcde": + # hidden_field(:token) + # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" /> # def hidden_field(method, options = {}) @emitted_hidden_id = true if method == :id @@ -1782,19 +1795,24 @@ module ActionView # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations. # # ==== Examples - # file_field(:user, :avatar) + # # Let's say that @user has avatar: + # file_field(:avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> # - # file_field(:post, :image, :multiple => true) - # # => <input type="file" id="post_image" name="post[image]" multiple="true" /> + # # Let's say that @post has image: + # file_field(:image, :multiple => true) + # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" /> # - # file_field(:post, :attached, accept: 'text/html') + # # Let's say that @post has attached: + # file_field(:attached, accept: 'text/html') # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> # - # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg') + # # Let's say that @post has image: + # file_field(:image, accept: 'image/png,image/gif,image/jpeg') # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" /> # - # file_field(:attachment, :file, class: 'file_input') + # # Let's say that @attachment has file: + # file_field(:file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> def file_field(method, options = {}) self.multipart = true @@ -1923,7 +1941,11 @@ module ActionView explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| - options[:child_index] = nested_child_index(name) unless explicit_child_index + if explicit_child_index + options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call) + else + options[:child_index] = nested_child_index(name) + end output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block) end output diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index bbfbf482a4..d3deee0df3 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -18,10 +18,10 @@ module ActionView # # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> + # <select name="post[category]" id="post_category"> + # <option value=""></option> + # <option value="joke">joke</option> + # <option value="poem">poem</option> # </select> # # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. @@ -32,7 +32,7 @@ module ActionView # # could become: # - # <select name="post[person_id]"> + # <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> @@ -45,7 +45,7 @@ module ActionView # # could become: # - # <select name="post[person_id]"> + # <select name="post[person_id]" id="post_person_id"> # <option value="">Select Person</option> # <option value="1">David</option> # <option value="2">Sam</option> @@ -71,19 +71,19 @@ module ActionView # # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # <option disabled="disabled">restricted</option> + # <select name="post[category]" id="post_category"> + # <option value=""></option> + # <option value="joke">joke</option> + # <option value="poem">poem</option> + # <option disabled="disabled" value="restricted">restricted</option> # </select> # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: - # <select name="post[category_id]"> + # <select name="post[category_id]" id="post_category_id"> # <option value="1" disabled="disabled">2008 stuff</option> # <option value="2" disabled="disabled">Christmas</option> # <option value="3">Jokes</option> @@ -109,7 +109,7 @@ module ActionView # # would become: # - # <select name="post[person_id]"> + # <select name="post[person_id]" id="post_person_id"> # <option value=""></option> # <option value="1" selected="selected">David</option> # <option value="2">Sam</option> @@ -192,7 +192,7 @@ module ActionView # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true) # # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: - # <select name="post[author_id]"> + # <select name="post[author_id]" id="post_author_id"> # <option value="">Please select</option> # <option value="1" selected="selected">D. Heinemeier Hansson</option> # <option value="2">D. Thomas</option> @@ -243,7 +243,7 @@ module ActionView # # Possible output: # - # <select name="city[country_id]"> + # <select name="city[country_id]" id="city_country_id"> # <optgroup label="Africa"> # <option value="1">South Africa</option> # <option value="3">Somalia</option> @@ -302,17 +302,17 @@ module ActionView # # => <option value="DKK">Kroner</option> # # options_for_select([ "VISA", "MasterCard" ], "MasterCard") - # # => <option>VISA</option> - # # => <option selected="selected">MasterCard</option> + # # => <option value="VISA">VISA</option> + # # => <option selected="selected" value="MasterCard">MasterCard</option> # # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") # # => <option value="$20">Basic</option> # # => <option value="$40" selected="selected">Plus</option> # # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) - # # => <option selected="selected">VISA</option> - # # => <option>MasterCard</option> - # # => <option selected="selected">Discover</option> + # # => <option selected="selected" value="VISA">VISA</option> + # # => <option value="MasterCard">MasterCard</option> + # # => <option selected="selected" value="Discover">Discover</option> # # You can optionally provide HTML attributes as the last element of the array. # @@ -410,7 +410,7 @@ module ActionView # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an # array of child objects representing the <tt><option></tt> tags. - # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a + # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. # * +option_key_method+ - The name of a method which, when called on a child object of a member of # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag. diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 93c04fbec6..1f76f40138 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -80,18 +80,17 @@ module ActionView # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box. # # ==== Options - # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. + # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty. # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something. - # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options. # * Any other key creates standard HTML attributes for the tag. # # ==== Examples # select_tag "people", options_from_collection_for_select(@people, "id", "name") # # <select id="people" name="people"><option value="1">David</option></select> # - # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"] + # select_tag "people", options_from_collection_for_select(@people, "id", "name", "1") # # <select id="people" name="people"><option value="1" selected="selected">David</option></select> # # select_tag "people", "<option>David</option>".html_safe @@ -777,10 +776,10 @@ module ActionView # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> # # number_field_tag 'quantity', nil, min: 1, max: 10 - # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # # => <input id="quantity" name="quantity" min="1" max="10" type="number" /> # # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2 - # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" /> + # # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" /> # # number_field_tag 'quantity', '1', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" /> diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index 629c447f3f..e237a32cb7 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -21,7 +21,7 @@ module ActionView # Also available through the alias j(). This is particularly helpful in JavaScript # responses, like: # - # $('some_element').replaceWith('<%=j render 'some/element_template' %>'); + # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) if javascript result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index f66dbfe7d3..13effa592d 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/string/output_safety' @@ -117,8 +116,8 @@ module ActionView # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -192,8 +191,8 @@ module ActionView # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -240,8 +239,8 @@ module ActionView # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +true+) # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -280,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 @@ -292,8 +291,8 @@ module ActionView # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +true+) # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb index 77c3e6d394..f7ee573035 100644 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ b/actionview/lib/action_view/helpers/record_tag_helper.rb @@ -1,108 +1,21 @@ -require 'action_view/record_identifier' - module ActionView - # = Action View Record Tag Helpers module Helpers module RecordTagHelper - include ActionView::RecordIdentifier - - # Produces a wrapper DIV element with id and class parameters that - # relate to the specified Active Record object. Usage example: - # - # <%= div_for(@person, class: "foo") do %> - # <%= @person.name %> - # <% end %> - # - # produces: - # - # <div id="person_123" class="person foo"> Joe Bloggs </div> - # - # You can also pass an array of Active Record objects, which will then - # get iterated over and yield each record as an argument for the block. - # For example: - # - # <%= div_for(@people, class: "foo") do |person| %> - # <%= person.name %> - # <% end %> - # - # produces: - # - # <div id="person_123" class="person foo"> Joe Bloggs </div> - # <div id="person_124" class="person foo"> Jane Bloggs </div> - # - def div_for(record, *args, &block) - content_tag_for(:div, record, *args, &block) + def div_for(*) + raise NoMethodError, "The `div_for` method has been removed from " \ + "Rails. To continue using it, add the `record_tag_helper` gem to " \ + "your Gemfile:\n" \ + " gem 'record_tag_helper', '~> 1.0'\n" \ + "Consult the Rails upgrade guide for details." end - # content_tag_for creates an HTML element with id and class parameters - # that relate to the specified Active Record object. For example: - # - # <%= content_tag_for(:tr, @person) do %> - # <td><%= @person.first_name %></td> - # <td><%= @person.last_name %></td> - # <% end %> - # - # would produce the following HTML (assuming @person is an instance of - # a Person object, with an id value of 123): - # - # <tr id="person_123" class="person">....</tr> - # - # If you require the HTML id attribute to have a prefix, you can specify it: - # - # <%= content_tag_for(:tr, @person, :foo) do %> ... - # - # produces: - # - # <tr id="foo_person_123" class="person">... - # - # You can also pass an array of objects which this method will loop through - # and yield the current object to the supplied block, reducing the need for - # having to iterate through the object (using <tt>each</tt>) beforehand. - # For example (assuming @people is an array of Person objects): - # - # <%= content_tag_for(:tr, @people) do |person| %> - # <td><%= person.first_name %></td> - # <td><%= person.last_name %></td> - # <% end %> - # - # produces: - # - # <tr id="person_123" class="person">...</tr> - # <tr id="person_124" class="person">...</tr> - # - # content_tag_for also accepts a hash of options, which will be converted to - # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined - # with the default class name for your object. For example: - # - # <%= content_tag_for(:li, @person, class: "bar") %>... - # - # produces: - # - # <li id="person_123" class="person bar">... - # - def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block) - options, prefix = prefix, nil if prefix.is_a?(Hash) - - Array(single_or_multiple_records).map do |single_record| - content_tag_for_single_record(tag_name, single_record, prefix, options, &block) - end.join("\n").html_safe + def content_tag_for(*) + raise NoMethodError, "The `content_tag_for` method has been removed from " \ + "Rails. To continue using it, add the `record_tag_helper` gem to " \ + "your Gemfile:\n" \ + " gem 'record_tag_helper', '~> 1.0'\n" \ + "Consult the Rails upgrade guide for details." end - - private - - # Called by <tt>content_tag_for</tt> internally to render a content tag - # for each record. - def content_tag_for_single_record(tag_name, record, prefix, options, &block) - options = options ? options.dup : {} - options[:class] = [ dom_class(record, prefix), options[:class] ].compact - options[:id] = dom_id(record, prefix) - - if block_given? - content_tag(tag_name, capture(record, &block), options) - else - content_tag(tag_name, "", options) - end - end end end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index e72e85ee5f..a2e9f37453 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -8,76 +8,77 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # This +sanitize+ helper will HTML encode all tags and strip all attributes that - # aren't specifically allowed. + # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. # - # It also strips href/src tags with invalid protocols, like javascript: especially. - # It does its best to counter any tricks that hackers may use, like throwing in - # unicode/ascii/hex values to get past the javascript: filters. Check out - # the extensive test suite. + # It also strips href/src attributes with unsafe protocols like + # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, + # ASCII, and hex character references to work around these protocol filters. # - # <%= sanitize @article.body %> + # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML + # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information. # - # You can add or remove tags/attributes if you want to customize it a bit. - # See ActionView::Base for full docs on the available options. You can add - # tags/attributes for single uses of +sanitize+ by passing either the - # <tt>:attributes</tt> or <tt>:tags</tt> options: + # Custom sanitization rules can also be provided. # - # Normal Use - # - # <%= sanitize @article.body %> + # Please note that sanitizing user-provided text does not guarantee that the + # resulting markup is valid or even well-formed. For example, the output may still + # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>. # - # Custom Use - Custom Scrubber - # (supply a Loofah::Scrubber that does the sanitization) + # ==== Options # - # scrubber can either wrap a block: - # scrubber = Loofah::Scrubber.new do |node| - # node.text = "dawn of cats" - # end + # * <tt>:tags</tt> - An array of allowed tags. + # * <tt>:attributes</tt> - An array of allowed attributes. + # * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer] + # or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that + # defines custom sanitization rules. A custom scrubber takes precedence over + # custom tags and attributes. # - # or be a subclass of Loofah::Scrubber which responds to scrub: - # class KittyApocalypse < Loofah::Scrubber - # def scrub(node) - # node.text = "dawn of cats" - # end - # end - # scrubber = KittyApocalypse.new + # ==== Examples # - # <%= sanitize @article.body, scrubber: scrubber %> + # Normal use: # - # A custom scrubber takes precedence over custom tags and attributes - # Learn more about scrubbers here: https://github.com/flavorjones/loofah + # <%= sanitize @comment.body %> # - # Custom Use - tags and attributes - # (only the mentioned tags and attributes are allowed, nothing else) + # Providing custom whitelisted tags and attributes: # - # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %> + # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> # - # Add table tags to the default allowed tags + # Providing a custom Rails::Html scrubber: # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_tags = ['table', 'tr', 'td'] - # end + # class CommentScrubber < Rails::Html::PermitScrubber + # def allowed_node?(node) + # !%w(form script comment blockquote).include?(node.name) + # end # - # Remove tags to the default allowed tags + # def skip_node?(node) + # node.text? + # end # - # class Application < Rails::Application - # config.after_initialize do - # ActionView::Base.sanitized_allowed_tags.delete 'div' + # def scrub_attribute?(name) + # name == 'style' # end # end # - # Change allowed default attributes + # <%= sanitize @comment.body, scrubber: CommentScrubber.new %> + # + # See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for + # documentation about Rails::Html scrubbers. # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style'] + # Providing a custom Loofah::Scrubber: + # + # scrubber = Loofah::Scrubber.new do |node| + # node.remove if node.name == 'script' # end # - # Please note that sanitizing user-provided text does not guarantee that the - # resulting markup is valid (conforming to a document type) or even well-formed. - # The output may still contain e.g. unescaped '<', '>', '&' characters and - # confuse browsers. + # <%= sanitize @comment.body, scrubber: scrubber %> + # + # See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more + # information about defining custom Loofah::Scrubber objects. # + # To set the default allowed tags or attributes across your application: + # + # # In config/application.rb + # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a'] + # config.action_view.sanitized_allowed_attributes = ['href', 'title'] def sanitize(html, options = {}) self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe) end @@ -87,9 +88,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from the +html+, including comments. This uses - # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability - # is limited by that of Nokogiri. + # Strips all HTML tags from +html+, including comments. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -100,10 +99,10 @@ module ActionView # strip_tags("<div id='top-bar'>Welcome to my website!</div>") # # => Welcome to my website! def strip_tags(html) - self.class.full_sanitizer.sanitize(html) + self.class.full_sanitizer.sanitize(html, encode_special_chars: false) end - # Strips all link tags from +text+ leaving just the link text. + # Strips all link tags from +html+ leaving just the link text. # # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') # # => Ruby on Rails @@ -166,30 +165,6 @@ module ActionView def white_list_sanitizer @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new end - - ## - # :method: sanitized_allowed_tags= - # - # :call-seq: sanitized_allowed_tags=(tags) - # - # Replaces the allowed tags for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_tags = ['table', 'tr', 'td'] - # end - # - - ## - # :method: sanitized_allowed_attributes= - # - # :call-seq: sanitized_allowed_attributes=(attributes) - # - # Replaces the allowed HTML attributes for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc'] - # end - # end end end diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb index 45c75d10c0..a4f6eb0150 100644 --- a/actionview/lib/action_view/helpers/tags.rb +++ b/actionview/lib/action_view/helpers/tags.rb @@ -5,6 +5,7 @@ module ActionView eager_autoload do autoload :Base + autoload :Translator autoload :CheckBox autoload :CollectionCheckBoxes autoload :CollectionRadioButtons diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index f8abb19698..d57f26ba4f 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -14,7 +14,7 @@ module ActionView @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") @object = retrieve_object(options.delete(:object)) @options = options - @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match + @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil end # This is what child classes implement. @@ -32,12 +32,19 @@ module ActionView unless object.nil? method_before_type_cast = @method_name + "_before_type_cast" - object.respond_to?(method_before_type_cast) ? - object.send(method_before_type_cast) : + if value_came_from_user?(object) && object.respond_to?(method_before_type_cast) + object.public_send(method_before_type_cast) + else value(object) + end end end + def value_came_from_user?(object) + method_name = "#{@method_name}_came_from_user?" + !object.respond_to?(method_name) || object.public_send(method_name) + end + def retrieve_object(object) if object object @@ -72,35 +79,30 @@ module ActionView end def add_default_name_and_id(options) - if options.has_key?("index") - options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } - options.delete("index") - elsif defined?(@auto_index) - options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } - else - options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id } + index = name_and_id_index(options) + options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) } + options["id"] = options.fetch("id"){ tag_id(index) } + if namespace = options.delete("namespace") + options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace end - - options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence - end - - def tag_name(multiple = false) - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" - end - - def tag_name_with_index(index, multiple = false) - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" end - def tag_id - "#{sanitized_object_name}_#{sanitized_method_name}" + def tag_name(multiple = false, index = nil) + # a little duplication to construct less strings + if index + "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" + else + "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" + end end - def tag_id_with_index(index) - "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + def tag_id(index = nil) + # a little duplication to construct less strings + if index + "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + else + "#{sanitized_object_name}_#{sanitized_method_name}" + end end def sanitized_object_name @@ -118,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) @@ -129,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) @@ -142,6 +150,10 @@ module ActionView end option_tags end + + def name_and_id_index(options) + options.key?("index") ? options.delete("index") || "" : @auto_index + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 6242a2a085..1765fa6558 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -41,14 +41,7 @@ module ActionView end def hidden_field - hidden_name = @html_options[:name] - - hidden_name ||= if @options.has_key?(:index) - "#{tag_name_with_index(@options[:index])}[]" - else - "#{tag_name}[]" - end - + hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" @template_object.hidden_field_tag(hidden_name, "", id: nil) end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index 08a23e497e..b31d5fda66 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -15,20 +15,10 @@ module ActionView def translation method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name - @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1') - - if object.respond_to?(:to_model) - key = object.model_name.i18n_key - i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] - end - - i18n_default ||= "" - content = I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence - - content ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(method_and_value) - end + content ||= Translator + .new(object, @object_name, method_and_value, scope: "helpers.label") + .translate content ||= @method_name.humanize content diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb index ae67bc13af..cf7b117614 100644 --- a/actionview/lib/action_view/helpers/tags/placeholderable.rb +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -7,24 +7,12 @@ module ActionView if tag_value = @options[:placeholder] placeholder = tag_value if tag_value.is_a?(String) - - object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}" - if object.respond_to?(:to_model) - key = object.class.model_name.i18n_key - i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] - end - - i18n_default ||= "" - placeholder ||= I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence - - placeholder ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(method_and_value) - end - + placeholder ||= Tags::Translator + .new(object, @object_name, method_and_value, scope: "helpers.placeholder") + .translate placeholder ||= @method_name.humanize - @options[:placeholder] = placeholder end end diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb new file mode 100644 index 0000000000..8b6655481d --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -0,0 +1,40 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Translator # :nodoc: + def initialize(object, object_name, method_and_value, scope:) + @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') + @method_and_value = method_and_value + @scope = scope + @model = object.respond_to?(:to_model) ? object.to_model : nil + end + + def translate + translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence + translated_attribute || human_attribute_name + end + + protected + + attr_reader :object_name, :method_and_value, :scope, :model + + private + + def i18n_default + if model + key = model.model_name.i18n_key + ["#{key}.#{method_and_value}".to_sym, ""] + else + "" + end + end + + def human_attribute_name + if model && model.class.respond_to?(:human_attribute_name) + model.class.human_attribute_name(method_and_value) + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 2c40ed1832..c216d4401f 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -103,7 +103,9 @@ module ActionView # Highlights one or more +phrases+ everywhere in +text+ by inserting it into # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt> # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to - # '<mark>\1</mark>') or passing a block that receives each matched term. + # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+ + # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false + # for <tt>:sanitize</tt> will turn sanitizing off. # # highlight('You searched for: rails', 'rails') # # => You searched for: <mark>rails</mark> @@ -122,6 +124,9 @@ module ActionView # # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) } # # => You searched for: <a href="search?q=rails">rails</a> + # + # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false) + # # => "<a>ruby</a> on <mark>rails</mark>" def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 342361217c..0615bd2e0d 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -7,48 +7,65 @@ module ActionView module Helpers module TranslationHelper include TagHelper - # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. + # Delegates to <tt>I18n#translate</tt> but also performs three additional + # functions. # - # First, it will ensure that any thrown +MissingTranslation+ messages will be turned - # into inline spans that: + # First, it will ensure that any thrown +MissingTranslation+ messages will + # be rendered as inline spans that: # - # * have a "translation-missing" class set, - # * contain the missing key as a title attribute and - # * a titleized version of the last key segment as a text. + # * Have a <tt>translation-missing</tt> class applied + # * Contain the missing key as the value of the +title+ attribute + # * Have a titleized version of the last key segment as text # - # E.g. the value returned for a missing translation key :"blog.post.title" will be - # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>. - # This way your views will display rather reasonable strings but it will still - # be easy to spot missing translations. + # For example, the value returned for the missing translation key + # <tt>"blog.post.title"</tt> will be: # - # Second, it'll scope the key by the current partial if the key starts - # with a period. So if you call <tt>translate(".foo")</tt> from the - # <tt>people/index.html.erb</tt> template, you'll actually be calling - # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive - # to translate many keys within the same partials and gives you a simple framework - # for scoping them consistently. If you don't prepend the key with a period, - # nothing is converted. + # <span + # class="translation_missing" + # title="translation missing: en.blog.post.title">Title</span> # - # Third, it'll mark the translation as safe HTML if the key has the suffix - # "_html" or the last element of the key is the word "html". For example, - # calling translate("footer_html") or translate("footer.html") will return - # a safe HTML string that won't be escaped by other HTML helper methods. This - # naming convention helps to identify translations that include HTML tags so that - # you know what kind of output to expect when you call translate in a template. + # This allows for views to display rather reasonable strings while still + # giving developers a way to find missing translations. + # + # If you would prefer missing translations to raise an error, you can + # opt out of span-wrapping behavior globally by setting + # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or + # individually by passing <tt>raise: true</tt> as an option to + # <tt>translate</tt>. + # + # Second, if the key starts with a period <tt>translate</tt> will scope + # the key by the current partial. Calling <tt>translate(".foo")</tt> from + # the <tt>people/index.html.erb</tt> template is equivalent to calling + # <tt>translate("people.index.foo")</tt>. This makes it less + # repetitive to translate many keys within the same partial and provides + # a convention to scope keys consistently. + # + # Third, the translation will be marked as <tt>html_safe</tt> if the key + # has the suffix "_html" or the last element of the key is "html". Calling + # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt> + # will return an HTML safe string that won't be escaped by other HTML + # helper methods. This naming convention helps to identify translations + # that include HTML tags so that you know what kind of output to expect + # when you call translate in a template and translators know which keys + # they can provide HTML values for. def translate(key, options = {}) options = options.dup - remaining_defaults = Array(options.delete(:default)) - options[:default] = remaining_defaults.shift if remaining_defaults.first.kind_of? String + has_default = options.has_key?(:default) + remaining_defaults = Array(options.delete(:default)).compact + + if has_default && !remaining_defaults.first.kind_of?(Symbol) + options[:default] = remaining_defaults + end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. # Otherwise, tell I18n to raise an exception, which we rescue further in this method. # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default. - if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?) + if options[:raise] == false raise_error = false - options[:raise] = false + i18n_raise = false else - raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations - options[:raise] = true + raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations + i18n_raise = true end if html_safe_translation_key?(key) @@ -58,11 +75,11 @@ module ActionView html_safe_options[name] = ERB::Util.html_escape(value.to_s) end end - translation = I18n.translate(scope_key_by_partial(key), html_safe_options) + translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) translation.respond_to?(:html_safe) ? translation.html_safe : translation else - I18n.translate(scope_key_by_partial(key), options) + I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end rescue I18n::MissingTranslationData => e if remaining_defaults.present? diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 8c2d5705f1..afb1265ad9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -46,9 +46,9 @@ module ActionView end protected :_back_url - # Creates a link tag of the given +name+ using a URL created by the set of +options+. + # Creates an anchor element of the given +name+ using a URL created by the set of +options+. # See the valid options in the documentation for +url_for+. It's also possible to - # pass a String instead of an options hash, which generates a link tag that uses the + # pass a String instead of an options hash, which generates an anchor element that uses the # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead # of an options hash will generate a link to the referrer (a JavaScript back link # will be used in place of a referrer if none exists). If +nil+ is passed as the name @@ -172,6 +172,11 @@ module ActionView # # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" } # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a> + # + # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>: + # + # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow" + # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a> def link_to(name = nil, options = nil, html_options = nil, &block) html_options, options, name = options, name, block if block_given? options ||= {} @@ -280,9 +285,7 @@ module ActionView html_options, options = options, name if block_given? options ||= {} html_options ||= {} - html_options = html_options.stringify_keys - convert_boolean_attributes!(html_options, %w(disabled)) url = options.is_a?(String) ? options : url_for(options) remote = html_options.delete('remote') @@ -294,8 +297,9 @@ module ActionView form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} form_options[:class] ||= html_options.delete('form_class') || 'button_to' - form_options.merge!(method: form_method, action: url) - form_options.merge!("data-remote" => "true") if remote + form_options[:method] = form_method + form_options[:action] = url + form_options[:'data-remote'] = true if remote request_token_tag = form_method == 'post' ? token_tag : '' @@ -459,7 +463,7 @@ module ActionView html_options = (html_options || {}).stringify_keys extras = %w{ cc bcc body subject reply_to }.map! { |item| - option = html_options.delete(item) || next + option = html_options.delete(item).presence || next "#{item.dasherize}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + extras.join('&') @@ -472,57 +476,45 @@ module ActionView # True if the current request URI was generated by the given +options+. # # ==== Examples - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action. + # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. # # current_page?(action: 'process') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # - # current_page?(controller: 'shop', action: 'checkout', order: 'asc') - # # => false - # # current_page?(action: 'checkout') # # => true # # current_page?(controller: 'library', action: 'checkout') # # => false # - # current_page?('http://www.example.com/shop/checkout') - # # => true - # - # current_page?('/shop/checkout') + # current_page?(controller: 'shop', action: 'checkout') # # => true # - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. - # - # current_page?(action: 'process') + # current_page?(controller: 'shop', action: 'checkout', order: 'asc') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1') # # => true # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2') # # => false # - # current_page?(controller: 'shop', action: 'checkout', order: 'desc') - # # => false + # current_page?('http://www.example.com/shop/checkout') + # # => true # - # current_page?(action: 'checkout') + # current_page?('/shop/checkout') # # => true # - # current_page?(controller: 'library', action: 'checkout') - # # => false + # current_page?('http://www.example.com/shop/checkout?order=desc&page=1') + # # => true # # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product. # # current_page?(controller: 'product', action: 'index') # # => false # + # We can also pass in the symbol arguments instead of strings. + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ @@ -576,34 +568,6 @@ module ActionView html_options["data-method"] = method end - # Processes the +html_options+ hash, converting the boolean - # attributes from true/false form into the form required by - # HTML/XHTML. (An attribute is considered to be boolean if - # its name is listed in the given +bool_attrs+ array.) - # - # More specifically, for each boolean attribute in +html_options+ - # given as: - # - # "attr" => bool_value - # - # if the associated +bool_value+ evaluates to true, it is - # replaced with the attribute's name; otherwise the attribute is - # removed from the +html_options+ hash. (See the XHTML 1.0 spec, - # section 4.5 "Attribute Minimization" for more: - # http://www.w3.org/TR/xhtml1/#h-4.5) - # - # Returns the updated +html_options+ hash, which is also modified - # in place. - # - # Example: - # - # convert_boolean_attributes!( html_options, - # %w( checked disabled readonly ) ) - def convert_boolean_attributes!(html_options, bool_attrs) - bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } - html_options - end - def token_tag(token=nil) if token != false && protect_against_forgery? token ||= form_authenticity_token diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 0b5c0b9991..9d636c8c9e 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -228,7 +228,7 @@ module ActionView # set by the <tt>layout</tt> method. # # ==== Returns - # * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise. + # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise. def _conditional_layout? return unless super diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 36855ec3d0..4452dcfed5 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -126,7 +126,7 @@ module ActionView @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) end - def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + def exists?(name, prefixes = [], partial = false, keys = [], **options) @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb index d42e436b17..b6ed13424e 100644 --- a/actionview/lib/action_view/model_naming.rb +++ b/actionview/lib/action_view/model_naming.rb @@ -1,5 +1,5 @@ module ActionView - module ModelNaming + module ModelNaming #:nodoc: # Converts the given object to an ActiveModel compliant one. def convert_to_model(object) object.respond_to?(:to_model) ? object.to_model : object diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 81f9c40b85..5dc7950d6b 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -36,14 +36,22 @@ module ActionView end end + initializer "action_view.collection_caching" do |app| + ActiveSupport.on_load(:action_controller) do + PartialRenderer.collection_cache = app.config.action_controller.cache_store + end + end + initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do - ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) + 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/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index c8484bed34..6c6e69101b 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -103,7 +103,7 @@ module ActionView # make sure yourself that your dom ids are valid, in case you overwrite this method. def record_key_for_dom_id(record) key = convert_to_model(record).to_key - key ? key.join('_') : key + key ? key.join(JOIN) : key end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 6c3015180a..b751bca31e 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,3 +1,4 @@ +require 'action_view/renderer/partial_renderer/collection_caching' require 'thread_safe' module ActionView @@ -153,23 +154,23 @@ module ActionView # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types # of users: # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # Here's the administrator: # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %> # # Here's the editor: # <%= render partial: "user", layout: "editor", locals: { user: editor } %> # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> # - # <%# app/views/users/_administrator.html.erb &> + # <%# app/views/users/_administrator.html.erb %> # <div id="administrator"> # Budget: $<%= user.budget %> # <%= yield %> # </div> # - # <%# app/views/users/_editor.html.erb &> + # <%# app/views/users/_editor.html.erb %> # <div id="editor"> # Deadline: <%= user.deadline %> # <%= yield %> @@ -232,7 +233,7 @@ module ActionView # # You can also apply a layout to a block within any template: # - # <%# app/views/users/_chief.html.erb &> + # <%# app/views/users/_chief.html.erb %> # <%= render(layout: "administrator", locals: { user: chief }) do %> # Title: <%= chief.title %> # <% end %> @@ -249,13 +250,13 @@ module ActionView # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass # an array to layout and treat it as an enumerable. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # Budget: $<%= user.budget %> # <%= yield user %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user| %> # Title: <%= user.title %> # <% end %> @@ -264,14 +265,14 @@ module ActionView # # You can also yield multiple times in one layout and use block arguments to differentiate the sections. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # <%= yield user, :header %> # Budget: $<%= user.budget %> # <%= yield user, :footer %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user, section| %> # <%- case section when :header -%> # Title: <%= user.title %> @@ -280,6 +281,8 @@ module ActionView # <%- end -%> # <% end %> class PartialRenderer < AbstractRenderer + include CollectionCaching + PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| h[k] = ThreadSafe::Cache.new end @@ -321,8 +324,9 @@ module ActionView spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) end - result = @template ? collection_with_template : collection_without_template - result.join(spacer).html_safe + cache_collection_render do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe end def render_partial @@ -334,7 +338,7 @@ module ActionView end object ||= locals[as] - locals[as] = object + locals[as] = object if @has_object content = @template.render(view, locals) do |*name| view._layout_for(*name, &block) @@ -519,7 +523,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin base = path[-1] == "/" ? "" : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ + raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ $1.to_sym end if @collection @@ -530,8 +534,7 @@ module ActionView end IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + - "make sure your partial name starts with underscore, " + - "and is followed by any combination of letters, numbers and underscores." + "make sure your partial name starts with underscore." OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " + "make sure it starts with lowercase letter, " + diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb new file mode 100644 index 0000000000..c8268e226e --- /dev/null +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -0,0 +1,70 @@ +require 'active_support/core_ext/object/try' + +module ActionView + module CollectionCaching # :nodoc: + extend ActiveSupport::Concern + + included do + # Fallback cache store if Action View is used without Rails. + # Otherwise overridden in Railtie to use Rails.cache. + mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new } + end + + private + def cache_collection_render + return yield unless cache_collection? + + keyed_collection = collection_by_cache_keys + partial_cache = collection_cache.read_multi(*keyed_collection.keys) + + @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values + rendered_partials = @collection.any? ? yield.dup : [] + + fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do + rendered_partials.shift + end + end + + def cache_collection? + @options.fetch(:cache, automatic_cache_eligible?) + end + + def automatic_cache_eligible? + single_template_render? && !callable_cache_key? && + @template.eligible_for_collection_caching?(as: @options[:as]) + end + + def single_template_render? + @template # Template is only set when a collection renders one template. + end + + def callable_cache_key? + @options[:cache].respond_to?(:call) + end + + def collection_by_cache_keys + seed = callable_cache_key? ? @options[:cache] : ->(i) { i } + + @collection.each_with_object({}) do |item, hash| + hash[expanded_cache_key(seed.call(item))] = item + end + end + + def expanded_cache_key(key) + key = @view.fragment_cache_key(@view.cache_fragment_name(key)) + key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. + end + + def fetch_or_cache_partial(cached_partials, order_by:) + cache_options = @options[:cache_options] || @locals[:cache_options] || {} + + order_by.map do |key| + cached_partials.fetch(key) do + yield.tap do |rendered_partial| + collection_cache.write(key, rendered_partial, cache_options) + end + end + end + end + end +end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 964b18337e..1bee35d80d 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -37,7 +37,7 @@ module ActionView end end - # Direct accessor to template rendering. + # Direct access to template rendering. def render_template(context, options) #:nodoc: TemplateRenderer.new(@lookup_context).render(context, options) end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index cd21d7ab47..dbb4855e39 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -40,7 +40,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." end end diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index abd3b77c67..1e8e7415d1 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -92,12 +92,15 @@ module ActionView # Find and render a template based on the options given. # :api: private def _render_template(options) #:nodoc: - variant = options[:variant] + variant = options.delete(:variant) + assigns = options.delete(:assigns) + context = view_context + context.assign assigns if assigns lookup_context.rendered_format = nil if options[:formats] lookup_context.variants = variant if variant - view_renderer.render(view_context, options) + view_renderer.render(context, options) end # Assign the rendered format to lookup context. diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index f281333a41..0371db07dc 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -130,5 +130,11 @@ module ActionView controller.optimize_routes_generation? : super end protected :optimize_routes_generation? + + private + + def _generate_paths_by_default + true + end end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index b39f7d583b..f394c319c1 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,20 +2,22 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies end - def template_name - ENV['TEMPLATE'].split('.', 2).first - end + class CacheDigests + def self.template_name + ENV['TEMPLATE'].split('.', 2).first + end - def finder - ApplicationController.new.lookup_context + def self.finder + ApplicationController.new.lookup_context + end end end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 6b61378a1f..377ceb534a 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -87,6 +87,19 @@ module ActionView # expected_encoding # ) + ## + # :method: local_assigns + # + # Returns a hash with the defined local variables. + # + # Given this sub template rendering: + # + # <%= render "shared/header", { headline: "Welcome", person: person } %> + # + # You can use +local_assigns+ in the sub templates to access the local variables: + # + # local_assigns[:headline] # => "Welcome" + eager_autoload do autoload :Error autoload :Handlers @@ -103,7 +116,7 @@ module ActionView # This finalizer is needed (and exactly with a proc inside another proc) # otherwise templates leak in development. - Finalizer = proc do |method_name, mod| + Finalizer = proc do |method_name, mod| # :nodoc: proc do mod.module_eval do remove_possible_method method_name @@ -117,6 +130,7 @@ module ActionView @source = source @identifier = identifier @handler = handler + @cache_name = extract_resource_cache_call_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -152,6 +166,10 @@ module ActionView @type ||= Types[@formats.first] if @formats.first end + def eligible_for_collection_caching?(as: nil) + @cache_name == (as || inferred_cache_name).to_s + end + # Receives a view object and return a template similar to self by using @virtual_path. # # This method is useful if you have a template object but it does not contain its source @@ -332,5 +350,14 @@ module ActionView payload = { virtual_path: @virtual_path, identifier: @identifier } ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block) end + + def extract_resource_cache_call_name + $1 if @handler.respond_to?(:resource_cache_call_pattern) && + @source =~ @handler.resource_cache_call_pattern + end + + def inferred_cache_name + @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '') + end end end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 85a100ed4c..88a8570706 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,6 +123,24 @@ module ActionView ).src end + # 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. + # + # <% cache notification do %> # => notification + # + # The pattern should support templates with a beginning comment: + # + # <%# Still extractable even though there's a comment %> + # <% cache notification do %> # => notification + # + # But fail to extract a name if a resource association is cached. + # + # <% cache notification.event do %> # => nil + def resource_cache_call_pattern + /\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/ + end + private def valid_encoding(string, encoding) diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 397c86014a..b08fb0870f 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,7 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - escaped = template.source.gsub(/:/, '\:') + escaped = template.source.gsub(':'.freeze, '\:'.freeze) '%q:' + escaped + ':;' end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index bc0db330ea..955118a554 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -270,7 +270,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" + # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}", # ) # # ==== Pattern format and variables diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 812b011bd7..06810ad14d 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -204,7 +204,7 @@ module ActionView def view @view ||= begin view = @controller.view_context - view.singleton_class.send :include, _helpers + view.singleton_class.include(_helpers) view.extend(Locals) view.rendered_views = self.rendered_views view.output_buffer = self.output_buffer diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 4aa56f60f7..4635c645d0 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -49,20 +49,6 @@ I18n.backend.store_translations 'pt-BR', {} ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') -FIXTURES = Pathname.new(FIXTURE_LOAD_PATH) - -module RackTestUtils - def body_to_string(body) - if body.respond_to?(:each) - str = "" - body.each {|s| str << s } - str - else - body - end - end - extend self -end module RenderERBUtils def view @@ -225,50 +211,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end end -# Temporary base class -class Rack::TestCase < ActionDispatch::IntegrationTest - def self.testing(klass = nil) - if klass - @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '') - else - @testing - end - end - - def get(thing, *args) - if thing.is_a?(Symbol) - super("#{self.class.testing}/#{thing}", *args) - else - super - end - end - - def assert_body(body) - assert_equal body, Array(response.body).join - end - - def assert_status(code) - assert_equal code, response.status - end - - def assert_response(body, status = 200, headers = {}) - assert_body body - assert_status status - headers.each do |header, value| - assert_header header, value - end - end - - def assert_content_type(type) - assert_equal type, response.headers["Content-Type"] - end - - def assert_header(name, value) - assert_equal value, response.headers[name] - end -end - -ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) +ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) module ActionController class Base @@ -338,8 +281,3 @@ def jruby_skip(message = '') end require 'mocha/setup' # FIXME: stop using mocha - -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted diff --git a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb deleted file mode 100644 index 84d0b7417e..0000000000 --- a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb +++ /dev/null @@ -1 +0,0 @@ -Hello from me5/index.erb
\ No newline at end of file diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index bd345fe873..64bc4c41d6 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'rbconfig' require 'active_support/core_ext/array/extract_options' # The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited @@ -150,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 @@ -263,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 563caee8a2..c4228b5683 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' -require "active_model" +require 'active_model' +require 'fileutils' class ApplicationController < ActionController::Base self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") @@ -31,6 +32,10 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end + + def cache_key + name.to_s + end end module Quiz @@ -353,7 +358,7 @@ class TestController < ApplicationController end def rendering_nothing_on_layout - render :nothing => true + head :ok end def render_to_string_with_assigns @@ -453,6 +458,10 @@ class TestController < ApplicationController render :text => "foo" end + def render_with_assigns_option + render inline: '<%= @hello %>', assigns: { hello: "world" } + end + def yield_content_for render :action => "content_for", :layout => "yield" end @@ -670,25 +679,32 @@ 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 @@ -704,20 +720,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 @@ -735,12 +749,19 @@ 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 - assert_raise ActionView::MissingTemplate do - get :render_action_upcased_hello_world + action = :render_action_upcased_hello_world + + if case_sensitive_file_system? + assert_raise ActionView::MissingTemplate do + get action + end + else + get action + assert_equal 'Hello world!', @response.body end end @@ -748,13 +769,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: @@ -857,12 +877,12 @@ class RenderTest < ActionController::TestCase # :ported: def test_attempt_to_access_object_method - assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone } + assert_raise(AbstractController::ActionNotFound) { get :clone } end # :ported: def test_private_methods - assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout } + assert_raise(AbstractController::ActionNotFound) { get :determine_layout } end # :ported: @@ -942,7 +962,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: @@ -953,23 +973,23 @@ class RenderTest < ActionController::TestCase end def test_accessing_params_in_template - get :accessing_params_in_template, :name => "David" + get :accessing_params_in_template, params: { name: "David" } assert_equal "Hello: David", @response.body end def test_accessing_local_assigns_in_inline_template - get :accessing_local_assigns_in_inline_template, :local_name => "Local David" + get :accessing_local_assigns_in_inline_template, params: { local_name: "Local David" } assert_equal "Goodbye, Local David", @response.body assert_equal "text/html", @response.content_type end def test_should_implicitly_render_html_template_from_xhr_request - xhr :get, :render_implicit_html_template_from_xhr_request + get :render_implicit_html_template_from_xhr_request, xhr: true assert_equal "XHR!\nHello HTML!", @response.body end def test_should_implicitly_render_js_template_without_layout - xhr :get, :render_implicit_js_template_without_layout, :format => :js + get :render_implicit_js_template_without_layout, format: :js, xhr: true assert_no_match %r{<html>}, @response.body end @@ -1027,8 +1047,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 @@ -1037,12 +1057,12 @@ 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 - get :accessing_params_in_template_with_layout, :name => "David" + get :accessing_params_in_template_with_layout, params: { name: "David" } assert_equal "<html>Hello: David</html>", @response.body end @@ -1099,7 +1119,12 @@ 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 + get :render_with_assigns_option + assert_equal 'world', response.body end # :ported: @@ -1160,22 +1185,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 @@ -1199,21 +1224,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 @@ -1257,48 +1279,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/active_record_unit.rb b/actionview/test/active_record_unit.rb index cca55c9af4..f9e94413b5 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -76,7 +76,7 @@ class ActiveRecordTestCase < ActionController::TestCase # Set our fixture path if ActiveRecordTestConnector.able_to_connect self.fixture_path = [FIXTURE_LOAD_PATH] - self.use_transactional_fixtures = false + self.use_transactional_tests = false end def self.fixtures(*args) diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 469adff39a..af91348d76 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -4,7 +4,7 @@ require 'fixtures/project' require 'active_support/log_subscriber/test_helper' require 'action_controller/log_subscriber' -ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime +ActionController::Base.include(ActiveRecord::Railties::ControllerRuntime) class ControllerRuntimeLogSubscriberTest < ActionController::TestCase class LogSubscriberController < ActionController::Base diff --git a/actionview/test/activerecord/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb index 5609694cd5..03cb1d5a91 100644 --- a/actionview/test/activerecord/debug_helper_test.rb +++ b/actionview/test/activerecord/debug_helper_test.rb @@ -1,8 +1,14 @@ require 'active_record_unit' +require 'nokogiri' class DebugHelperTest < ActionView::TestCase def test_debug company = Company.new(name: "firebase") assert_match "name: firebase", debug(company) end + + def test_debug_with_marshal_error + obj = -> { } + assert_match obj.inspect, Nokogiri.XML(debug(obj)).content + end end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 0a62f49f35..2769b97445 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -35,10 +35,6 @@ class FormHelperActiveRecordTest < ActionView::TestCase end end - def _routes - Routes - end - include Routes.url_helpers def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index 8e1ed2776d..34b2698c7f 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -113,7 +113,7 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_passing_routes_proxy with_namespaced_routes(:blog) do - proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self) + proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self, _routes.url_helpers) @blog_post.save assert_url "http://example.com/posts/#{@blog_post.id}", [proxy, @blog_post] end @@ -208,7 +208,7 @@ class PolymorphicRoutesTest < ActionController::TestCase @series.save polymorphic_url([nil, @series]) end - assert_match(/undefined method `series_url' for/, exception.message) + assert_match(/undefined method `series_url'/, exception.message) end end diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb index 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/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/layouts/streaming_with_capture.erb b/actionview/test/fixtures/layouts/streaming_with_capture.erb new file mode 100644 index 0000000000..538c19ce3a --- /dev/null +++ b/actionview/test/fixtures/layouts/streaming_with_capture.erb @@ -0,0 +1,6 @@ +<%= yield :header -%> +<%= capture do %> + this works +<% end %> +<%= yield :footer -%> +<%= yield(:unknown).presence || "." -%> diff --git a/actionview/test/fixtures/multipart/bracketed_utf8_param b/actionview/test/fixtures/multipart/bracketed_utf8_param deleted file mode 100644 index df9cecea08..0000000000 --- a/actionview/test/fixtures/multipart/bracketed_utf8_param +++ /dev/null @@ -1,5 +0,0 @@ ---AaB03x -Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]" - -Iñtërnâtiônàlizætiøn_value ---AaB03x-- diff --git a/actionview/test/fixtures/multipart/single_utf8_param b/actionview/test/fixtures/multipart/single_utf8_param deleted file mode 100644 index 1d9fae7b17..0000000000 --- a/actionview/test/fixtures/multipart/single_utf8_param +++ /dev/null @@ -1,5 +0,0 @@ ---AaB03x -Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name" - -Iñtërnâtiônàlizætiøn_value ---AaB03x-- diff --git a/actionview/test/fixtures/test/_FooBar.html.erb b/actionview/test/fixtures/test/_FooBar.html.erb new file mode 100644 index 0000000000..4bbe59410a --- /dev/null +++ b/actionview/test/fixtures/test/_FooBar.html.erb @@ -0,0 +1 @@ +🍣 diff --git a/actionview/test/fixtures/test/_a-in.html.erb b/actionview/test/fixtures/test/_a-in.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/test/_a-in.html.erb diff --git a/actionview/test/fixtures/test/_cached_customer.erb b/actionview/test/fixtures/test/_cached_customer.erb new file mode 100644 index 0000000000..52f35a3497 --- /dev/null +++ b/actionview/test/fixtures/test/_cached_customer.erb @@ -0,0 +1,3 @@ +<% cache cached_customer do %> + Hello: <%= cached_customer.name %> +<% end %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_cached_customer_as.erb b/actionview/test/fixtures/test/_cached_customer_as.erb new file mode 100644 index 0000000000..fca8d19e34 --- /dev/null +++ b/actionview/test/fixtures/test/_cached_customer_as.erb @@ -0,0 +1,3 @@ +<% cache buyer do %> + <%= greeting %>: <%= customer.name %> +<% end %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb new file mode 100644 index 0000000000..28ee9f41c5 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb @@ -0,0 +1 @@ +<%= local_assigns.has_key?(:partial_name_in_local_assigns) %>
\ No newline at end of file diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index 789b1d198b..65c68fc34a 100644 --- a/actionview/test/lib/controller/fake_models.rb +++ b/actionview/test/lib/controller/fake_models.rb @@ -54,6 +54,22 @@ class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :writt def tags_attributes=(attributes); end end +class PostDelegator < Post + def to_model + PostDelegate.new + end +end + +class PostDelegate < Post + def self.human_attribute_name(attribute) + "Delegate #{super}" + end + + def model_name + ActiveModel::Name.new(self.class) + end +end + class Comment extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index dac1c7024d..6e6ce20924 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -1,4 +1,3 @@ -require 'zlib' require 'abstract_unit' require 'active_support/ordered_options' @@ -180,6 +179,7 @@ class AssetTagHelperTest < ActionView::TestCase %(image_tag("xml.png")) => %(<img alt="Xml" src="/images/xml.png" />), %(image_tag("rss.gif", :alt => "rss syndication")) => %(<img alt="rss syndication" src="/images/rss.gif" />), %(image_tag("gold.png", :size => "20")) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />), + %(image_tag("gold.png", :size => 20)) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />), %(image_tag("gold.png", :size => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />), %(image_tag("gold.png", "size" => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />), %(image_tag("error.png", "size" => "45 x 70")) => %(<img alt="Error" src="/images/error.png" />), @@ -238,6 +238,7 @@ class AssetTagHelperTest < ActionView::TestCase %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>), %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>), %(video_tag("error.avi", "size" => "100")) => %(<video height="100" src="/videos/error.avi" width="100"></video>), + %(video_tag("error.avi", "size" => 100)) => %(<video height="100" src="/videos/error.avi" width="100"></video>), %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>), %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>), %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>), @@ -463,6 +464,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal({:size => '16x10'}, options) end + def test_image_tag_raises_an_error_for_competing_size_arguments + exception = assert_raise(ArgumentError) do + image_tag("gold.png", :height => "100", :width => "200", :size => "45x70") + end + + assert_equal("Cannot pass a :size option with a :height or :width option", exception.message) + end + def test_favicon_link_tag FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 68b44c4f0d..591cd71404 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -14,7 +14,7 @@ class ScrollsController < ActionController::Base FEEDS["defaults"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -31,7 +31,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_options"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry| @@ -48,7 +48,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_type_options"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :type => 'text/xml') do |entry| @@ -62,10 +62,27 @@ class ScrollsController < ActionController::Base end end EOT + FEEDS["entry_url_false_option"] = <<-EOT + atom_feed do |feed| + feed.title("My great blog!") + feed.updated(@scrolls.first.created_at) + + @scrolls.each do |scroll| + feed.entry(scroll, :url => false) do |entry| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT FEEDS["xml_block"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) feed.author do |author| author.name("DHH") @@ -83,7 +100,7 @@ class ScrollsController < ActionController::Base atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app', 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -101,7 +118,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_overridden_ids"] = <<-EOT atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry| @@ -120,7 +137,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -138,7 +155,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -155,7 +172,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_xhtml_content"] = <<-'EOT' atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -180,7 +197,7 @@ class ScrollsController < ActionController::Base new_xml = Builder::XmlMarkup.new(:target=>'') atom_feed(:xml => new_xml) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -214,28 +231,28 @@ class AtomFeedTest < ActionController::TestCase def test_feed_should_use_default_language_if_none_is_given with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_match(%r{xml:lang="en-US"}, @response.body) end end def test_feed_should_include_two_entries with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_select "entry", 2 end end def test_entry_should_only_use_published_if_created_at_is_present with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_select "published", 1 end end def test_providing_builder_to_atom_feed with_restful_routing(:scrolls) do - get :index, :id=>"provide_builder" + get :index, params: { id: "provide_builder" } # because we pass in the non-default builder, the content generated by the # helper should go 'nowhere'. Leaving the response body blank. assert @response.body.blank? @@ -244,7 +261,7 @@ class AtomFeedTest < ActionController::TestCase def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record with_restful_routing(:scrolls) do - get :index, :id => "entry_options" + get :index, params: { id: "entry_options" } assert_select "updated", Time.utc(2007, 1, 1).xmlschema assert_select "updated", Time.utc(2007, 1, 2).xmlschema @@ -253,21 +270,21 @@ class AtomFeedTest < ActionController::TestCase def test_self_url_should_default_to_current_request_url with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_select "link[rel=self][href=\"http://www.nextangle.com/scrolls?id=defaults\"]" end end def test_feed_id_should_be_a_valid_tag with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_select "id", :text => "tag:www.nextangle.com,2008:/scrolls?id=defaults" end end def test_entry_id_should_be_a_valid_tag with_restful_routing(:scrolls) do - get :index, :id => "defaults" + get :index, params: { id: "defaults" } assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/1" assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/2" end @@ -275,14 +292,14 @@ class AtomFeedTest < ActionController::TestCase def test_feed_should_allow_nested_xml_blocks with_restful_routing(:scrolls) do - get :index, :id => "xml_block" + get :index, params: { id: "xml_block" } assert_select "author name", :text => "DHH" end end def test_feed_should_include_atomPub_namespace with_restful_routing(:scrolls) do - get :index, :id => "feed_with_atomPub_namespace" + get :index, params: { id: "feed_with_atomPub_namespace" } assert_match %r{xml:lang="en-US"}, @response.body assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body @@ -291,7 +308,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_should_allow_overriding_ids with_restful_routing(:scrolls) do - get :index, :id => "feed_with_overridden_ids" + get :index, params: { id: "feed_with_overridden_ids" } assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/" assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1" assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2" @@ -300,7 +317,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_xml_processing_instructions with_restful_routing(:scrolls) do - get :index, :id => 'feed_with_xml_processing_instructions' + get :index, params: { id: 'feed_with_xml_processing_instructions' } assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body end @@ -308,7 +325,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_xml_processing_instructions_duplicate_targets with_restful_routing(:scrolls) do - get :index, :id => 'feed_with_xml_processing_instructions_duplicate_targets' + get :index, params: { id: 'feed_with_xml_processing_instructions_duplicate_targets' } assert_match %r{<\?target1 (a="1" b="2"|b="2" a="1")\?>}, @response.body assert_match %r{<\?target1 (c="3" d="4"|d="4" c="3")\?>}, @response.body end @@ -316,7 +333,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_xhtml with_restful_routing(:scrolls) do - get :index, :id => "feed_with_xhtml_content" + get :index, params: { id: "feed_with_xhtml_content" } assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body assert_select "summary", :text => /Something Boring/ assert_select "summary", :text => /after 2/ @@ -325,18 +342,25 @@ class AtomFeedTest < ActionController::TestCase def test_feed_entry_type_option_default_to_text_html with_restful_routing(:scrolls) do - get :index, :id => 'defaults' + get :index, params: { id: 'defaults' } assert_select "entry link[rel=alternate][type=\"text/html\"]" end end def test_feed_entry_type_option_specified with_restful_routing(:scrolls) do - get :index, :id => 'entry_type_options' + get :index, params: { id: 'entry_type_options' } assert_select "entry link[rel=alternate][type=\"text/xml\"]" end end + def test_feed_entry_url_false_option_adds_no_link + with_restful_routing(:scrolls) do + get :index, params: { id: 'entry_url_false_option' } + assert_select "entry link", false + end + end + private def with_restful_routing(resources) with_routing do |set| diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index f213da5934..1e099d482c 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -210,10 +210,4 @@ class CaptureHelperTest < ActionView::TestCase def alt_encoding(output_buffer) output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII end - - def view_with_controller - TestController.new.view_context.tap do |view| - view.output_buffer = ActionView::OutputBuffer.new - end - end end diff --git a/actionview/test/template/controller_helper_test.rb b/actionview/test/template/controller_helper_test.rb new file mode 100644 index 0000000000..b5e94ea4f1 --- /dev/null +++ b/actionview/test/template/controller_helper_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +class ControllerHelperTest < ActionView::TestCase + tests ActionView::Helpers::ControllerHelper + + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + def test_assign_controller_sets_default_form_builder + @controller = OpenStruct.new(default_form_builder: SpecializedFormBuilder) + assign_controller(@controller) + + assert_equal SpecializedFormBuilder, self.default_form_builder + end + + def test_assign_controller_skips_default_form_builder + @controller = OpenStruct.new + assign_controller(@controller) + + assert_nil self.default_form_builder + end +end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index bfb073680e..9212420ec9 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -130,7 +130,7 @@ class DateHelperTest < ActionView::TestCase def test_distance_in_words_with_mathn_required # test we avoid Integer#/ (redefined by mathn) - require 'mathn' + silence_warnings { require "mathn" } from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) end @@ -3217,12 +3217,4 @@ class DateHelperTest < ActionView::TestCase expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>' assert_equal expected, time_tag(time, :format => :short) end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index bb375076c6..672b4747ec 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'action_view/dependency_tracker' @@ -61,7 +60,6 @@ class ERBTrackerTest < Minitest::Test end def test_dependency_of_template_partial_with_layout - skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple! template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb) tracker = make_tracker("multiple/_dependencies", template) diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index c2b8439df3..f0afcdb5ae 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -111,6 +111,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") @@ -298,6 +310,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 diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index fff1e1e572..b8cb5bd746 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -40,6 +40,9 @@ class FormHelperTest < ActionView::TestCase }, tag: { value: "Tag" + }, + post_delegate: { + title: 'Delegate model_name title' } } } @@ -81,6 +84,9 @@ class FormHelperTest < ActionView::TestCase body: "Write body here" } }, + post_delegate: { + title: 'Delegate model_name title' + }, tag: { value: "Tag" } @@ -99,7 +105,9 @@ class FormHelperTest < ActionView::TestCase }.new end def @post.to_key; [123]; end - def @post.id_before_type_cast; 123; end + def @post.id; 0; end + def @post.id_before_type_cast; "omg"; end + def @post.id_came_from_user?; true; end def @post.to_param; '123'; end @post.persisted = true @@ -115,6 +123,10 @@ class FormHelperTest < ActionView::TestCase @post.tags = [] @post.tags << Tag.new + @post_delegator = PostDelegator.new + + @post_delegator.title = 'Hello World' + @car = Car.new("#000FFF") end @@ -247,6 +259,18 @@ class FormHelperTest < ActionView::TestCase end end + def test_label_with_non_active_record_object + form_for(OpenStruct.new(name:'ok'), as: 'person', url: 'an_url', html: { id: 'create-person' }) do |f| + f.label(:name) + end + + expected = whole_form("an_url", "create-person", "new_person", method: "post") do + '<label for="person_name">Name</label>' + end + + assert_dom_equal expected, output_buffer + end + def test_label_with_for_attribute_as_symbol assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, for: "my_for")) end @@ -335,6 +359,22 @@ class FormHelperTest < ActionView::TestCase ) end + def test_label_with_to_model + assert_dom_equal( + %{<label for="post_delegator_title">Delegate Title</label>}, + label(:post_delegator, :title) + ) + end + + def test_label_with_to_model_and_overriden_model_name + with_locale :label do + assert_dom_equal( + %{<label for="post_delegator_title">Delegate model_name title</label>}, + label(:post_delegator, :title) + ) + end + end + def test_text_field_placeholder_without_locales with_locale :placeholder do assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true)) @@ -347,12 +387,28 @@ class FormHelperTest < ActionView::TestCase end end + def test_text_field_placeholder_with_locales_and_to_model + with_locale :placeholder do + assert_dom_equal( + '<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate model_name title" type="text" value="Hello World" />', + text_field(:post_delegator, :title, placeholder: true) + ) + end + end + def test_text_field_placeholder_with_human_attribute_name with_locale :placeholder do assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true)) end end + def test_text_field_placeholder_with_human_attribute_name_and_to_model + assert_dom_equal( + '<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate Title" type="text" value="Hello World" />', + text_field(:post_delegator, :title, placeholder: true) + ) + end + def test_text_field_placeholder_with_string_value with_locale :placeholder do assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?")) @@ -900,9 +956,29 @@ class FormHelperTest < ActionView::TestCase ) end - def test_text_area_with_value_before_type_cast + def test_inputs_use_before_type_cast_to_retain_information_from_validations_like_numericality + assert_dom_equal( + %{<textarea id="post_id" name="post[id]">\nomg</textarea>}, + text_area("post", "id") + ) + end + + def test_inputs_dont_use_before_type_cast_when_value_did_not_come_from_user + class << @post + undef id_came_from_user? + def id_came_from_user?; false; end + end + + assert_dom_equal( + %{<textarea id="post_id" name="post[id]">\n0</textarea>}, + text_area("post", "id") + ) + end + + def test_inputs_use_before_typecast_when_object_doesnt_respond_to_came_from_user + class << @post; undef id_came_from_user?; end assert_dom_equal( - %{<textarea id="post_id" name="post[id]">\n123</textarea>}, + %{<textarea id="post_id" name="post[id]">\nomg</textarea>}, text_area("post", "id") ) end @@ -2802,6 +2878,23 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + class FakeAssociationProxy def to_ary [1, 2, 3] @@ -3176,6 +3269,30 @@ class FormHelperTest < ActionView::TestCase ActionView::Base.default_form_builder = old_default_form_builder end + def test_form_builder_override + self.default_form_builder = LabelledFormBuilder + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_lazy_loading_form_builder_override + self.default_form_builder = "FormHelperTest::LabelledFormBuilder" + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + def test_fields_for_with_labelled_builder output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| concat f.text_field(:title) 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 84a581b107..cad1c82309 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -210,13 +210,13 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag_with_multiple - actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, :multiple => :true - expected = %(<select id="colors" multiple="multiple" name="colors"><option>Red</option><option>Blue</option><option>Green</option></select>) + actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, multiple: true + expected = %(<select id="colors" multiple="multiple" name="colors[]"><option>Red</option><option>Blue</option><option>Green</option></select>) assert_dom_equal expected, actual end def test_select_tag_disabled - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :disabled => :true + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, disabled: true expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end @@ -352,7 +352,7 @@ class FormTagHelperTest < ActionView::TestCase end def test_text_field_disabled - actual = text_field_tag "title", "Hello!", :disabled => :true + actual = text_field_tag "title", "Hello!", disabled: true expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />) assert_dom_equal expected, actual end diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb index 9ba7f64ad1..9f1535ef53 100644 --- a/actionview/test/template/javascript_helper_test.rb +++ b/actionview/test/template/javascript_helper_test.rb @@ -3,14 +3,7 @@ require 'abstract_unit' class JavaScriptHelperTest < ActionView::TestCase tests ActionView::Helpers::JavaScriptHelper - def _evaluate_assigns_and_ivars() end - - attr_accessor :formats, :output_buffer - - def update_details(details) - @details = details - yield if block_given? - end + attr_accessor :output_buffer setup do @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index b59883b760..b70b750869 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -35,6 +35,10 @@ class NumberHelperTest < ActionView::TestCase assert_equal "98a%", number_to_percentage("98a") assert_equal "NaN%", number_to_percentage(Float::NAN) assert_equal "Inf%", number_to_percentage(Float::INFINITY) + assert_equal "NaN%", number_to_percentage(Float::NAN, precision: 0) + assert_equal "Inf%", number_to_percentage(Float::INFINITY, precision: 0) + assert_equal "NaN%", number_to_percentage(Float::NAN, precision: 1) + assert_equal "Inf%", number_to_percentage(Float::INFINITY, precision: 1) end def test_number_with_delimiter diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb index ab84bccb56..4b7b653916 100644 --- a/actionview/test/template/record_tag_helper_test.rb +++ b/actionview/test/template/record_tag_helper_test.rb @@ -24,94 +24,10 @@ class RecordTagHelperTest < ActionView::TestCase end def test_content_tag_for - expected = %(<li class="record_tag_post" id="record_tag_post_45"></li>) - actual = content_tag_for(:li, @post) - assert_dom_equal expected, actual + assert_raises(NoMethodError) { content_tag_for(:li, @post) } end - def test_content_tag_for_prefix - expected = %(<ul class="archived_record_tag_post" id="archived_record_tag_post_45"></ul>) - actual = content_tag_for(:ul, @post, :archived) - assert_dom_equal expected, actual - end - - def test_content_tag_for_with_extra_html_options - expected = %(<tr class="record_tag_post special" id="record_tag_post_45" style='background-color: #f0f0f0'></tr>) - actual = content_tag_for(:tr, @post, class: "special", style: "background-color: #f0f0f0") - assert_dom_equal expected, actual - end - - def test_content_tag_for_with_array_css_class - expected = %(<tr class="record_tag_post special odd" id="record_tag_post_45"></tr>) - actual = content_tag_for(:tr, @post, class: ["special", "odd"]) - assert_dom_equal expected, actual - end - - def test_content_tag_for_with_prefix_and_extra_html_options - expected = %(<tr class="archived_record_tag_post special" id="archived_record_tag_post_45" style='background-color: #f0f0f0'></tr>) - actual = content_tag_for(:tr, @post, :archived, class: "special", style: "background-color: #f0f0f0") - assert_dom_equal expected, actual - end - - def test_block_not_in_erb_multiple_calls - expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>) - actual = div_for(@post, class: "special") { @post.body } - assert_dom_equal expected, actual - actual = div_for(@post, class: "special") { @post.body } - assert_dom_equal expected, actual - end - - def test_block_works_with_content_tag_for_in_erb - expected = %(<tr class="record_tag_post" id="record_tag_post_45">What a wonderful world!</tr>) - actual = render_erb("<%= content_tag_for(:tr, @post) do %><%= @post.body %><% end %>") - assert_dom_equal expected, actual - end - - def test_div_for_in_erb - expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>) - actual = render_erb("<%= div_for(@post, class: 'special') do %><%= @post.body %><% end %>") - assert_dom_equal expected, actual - end - - def test_content_tag_for_collection - post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" } - post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" } - expected = %(<li class="record_tag_post" id="record_tag_post_101">Hello!</li>\n<li class="record_tag_post" id="record_tag_post_102">World!</li>) - actual = content_tag_for(:li, [post_1, post_2]) { |post| post.body } - assert_dom_equal expected, actual - end - - def test_content_tag_for_collection_without_given_block - post_1 = RecordTagPost.new.tap { |post| post.id = 101; post.body = "Hello!" } - post_2 = RecordTagPost.new.tap { |post| post.id = 102; post.body = "World!" } - expected = %(<li class="record_tag_post" id="record_tag_post_101"></li>\n<li class="record_tag_post" id="record_tag_post_102"></li>) - actual = content_tag_for(:li, [post_1, post_2]) - assert_dom_equal expected, actual - end - - def test_div_for_collection - post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" } - post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" } - expected = %(<div class="record_tag_post" id="record_tag_post_101">Hello!</div>\n<div class="record_tag_post" id="record_tag_post_102">World!</div>) - actual = div_for([post_1, post_2]) { |post| post.body } - assert_dom_equal expected, actual - end - - def test_content_tag_for_single_record_is_html_safe - result = div_for(@post, class: "special") { @post.body } - assert result.html_safe? - end - - def test_content_tag_for_collection_is_html_safe - post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" } - post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" } - result = content_tag_for(:li, [post_1, post_2]) { |post| post.body } - assert result.html_safe? - end - - def test_content_tag_for_does_not_change_options_hash - options = { class: "important" } - content_tag_for(:li, @post, options) - assert_equal({ class: "important" }, options) + def test_div_for + assert_raises(NoMethodError) { div_for(@post, class: "special") } end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index dc4abca048..27bbb9b6c1 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'controller/fake_models' @@ -62,9 +61,10 @@ module RenderTestCases def test_render_template_with_a_missing_partial_of_another_format @view.lookup_context.formats = [:html] - assert_raise ActionView::Template::Error, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do + e = assert_raise ActionView::Template::Error do @view.render(:template => "with_format", :formats => [:json]) end + assert_includes(e.message, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:raw, :erb, :builder, :ruby]}.") end def test_render_file_with_locale @@ -172,18 +172,12 @@ module RenderTestCases assert_equal "only partial", @view.render("test/partial_only", :counter_counter => 5) end - def test_render_partial_with_invalid_name - e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") } - assert_equal "The partial name (test/200) is not a valid Ruby identifier; " + - "make sure your partial name starts with underscore, " + - "and is followed by any combination of letters, numbers and underscores.", e.message + def test_render_partial_with_number + assert_nothing_raised { @view.render(:partial => "test/200") } end def test_render_partial_with_missing_filename - e = assert_raises(ArgumentError) { @view.render(:partial => "test/") } - assert_equal "The partial name (test/) is not a valid Ruby identifier; " + - "make sure your partial name starts with underscore, " + - "and is followed by any combination of letters, numbers and underscores.", e.message + assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/") } end def test_render_partial_with_incompatible_object @@ -191,11 +185,12 @@ module RenderTestCases assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message end + def test_render_partial_starting_with_a_capital + assert_nothing_raised { @view.render(:partial => 'test/FooBar') } + end + def test_render_partial_with_hyphen - e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in") } - assert_equal "The partial name (test/a-in) is not a valid Ruby identifier; " + - "make sure your partial name starts with underscore, " + - "and is followed by any combination of letters, numbers and underscores.", e.message + assert_nothing_raised { @view.render(:partial => "test/a-in") } end def test_render_partial_with_invalid_option_as @@ -282,6 +277,14 @@ module RenderTestCases assert_nil @view.render(:partial => "test/customer", :collection => nil) end + def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns + assert_equal 'false', @view.render(partial: 'test/partial_name_in_local_assigns') + end + + def test_render_partial_with_nil_object_puts_partial_name_to_local_assigns + assert_equal 'true', @view.render(partial: 'test/partial_name_in_local_assigns', object: nil) + end + def test_render_partial_with_nil_values_in_collection assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ]) end @@ -603,3 +606,41 @@ class LazyViewRenderTest < ActiveSupport::TestCase silence_warnings { Encoding.default_external = old } end end + +class CachedCollectionViewRenderTest < CachedViewRenderTest + class CachedCustomer < Customer; end + + teardown do + ActionView::PartialRenderer.collection_cache.clear + end + + test "with custom key" do + customer = Customer.new("david") + key = ActionController::Base.new.fragment_cache_key([customer, 'key']) + + ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + + assert_equal "Hello", + @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) + end + + test "automatic caching with inferred cache name" do + customer = CachedCustomer.new("david") + key = ActionController::Base.new.fragment_cache_key(customer) + + ActionView::PartialRenderer.collection_cache.write(key, 'Cached') + + assert_equal "Cached", + @view.render(partial: "test/cached_customer", collection: [customer]) + end + + test "automatic caching with as name" do + customer = CachedCustomer.new("david") + key = ActionController::Base.new.fragment_cache_key(customer) + + ActionView::PartialRenderer.collection_cache.write(key, 'Cached') + + assert_equal "Cached", + @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer) + end +end diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index e4be21be2c..efe846a7eb 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -29,6 +29,10 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal "", strip_tags("<script>") end + def test_strip_tags_will_not_encode_special_characters + assert_equal "test\r\n\r\ntest", strip_tags("test\r\n\r\ntest") + end + def test_sanitize_is_marked_safe assert sanitize("<html><script></script></html>").html_safe? end diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index 8a24d78e74..d06ba4ceb0 100644 --- a/actionview/test/template/streaming_render_test.rb +++ b/actionview/test/template/streaming_render_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' class TestController < ActionController::Base @@ -105,4 +104,8 @@ class FiberedTest < ActiveSupport::TestCase buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming") end + def test_render_with_streaming_and_capture + assert_equal "Yes, \n this works\n like a charm.", + buffered_render(template: "test/streaming", layout: "layouts/streaming_with_capture") + end end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index c94508d678..aae6a9aa09 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -183,10 +183,11 @@ class TestERBTemplate < ActiveSupport::TestCase end def test_error_when_template_isnt_valid_utf8 - assert_raises(ActionView::Template::Error, /\xFC/) do + e = assert_raises ActionView::Template::Error do @template = new_template("hello \xFCmlat", :virtual_path => nil) render end + assert_match(/\xFC/, e.message) end def with_external_encoding(encoding) diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 5ad1938b61..05c3dc0613 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -306,63 +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") - assert_raise ActiveSupport::TestCase::Assertion, /Somebody else.*David/m do - assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" } - end - 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 f05b845e46..f1b84c4786 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' class TextHelperTest < ActionView::TestCase diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index 8fde478ac9..5dc281adb2 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -41,8 +41,8 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.backend.reload! end - def test_delegates_to_i18n_setting_the_rescue_format_option_to_html - I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("") + def test_delegates_setting_to_i18n + I18n.expects(:translate).with(:foo, :locale => 'en', :raise => true).returns("") translate :foo, :locale => 'en' end @@ -58,12 +58,6 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translate(:"translations.missing").html_safe? end - def test_returns_missing_translation_message_using_nil_as_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, translate(:"translations.missing", :rescue_format => nil) - assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe? - end - def test_raises_missing_translation_message_with_raise_config_option ActionView::Base.raise_on_missing_translations = true @@ -96,12 +90,6 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.exception_handler = old_exception_handler end - def test_i18n_translate_defaults_to_nil_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, I18n.translate(:"translations.missing") - assert_equal false, I18n.translate(:"translations.missing").html_safe? - end - def test_translation_returning_an_array expected = %w(foo bar) assert_equal expected, translate(:"translations.array") @@ -157,6 +145,19 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translation.html_safe? end + def test_translate_with_missing_default + translation = translate(:'translations.missing', :default => :'translations.missing_html') + expected = '<span class="translation_missing" title="translation missing: en.translations.missing_html">Missing Html</span>' + assert_equal expected, translation + assert_equal true, translation.html_safe? + end + + def test_translate_with_missing_default_and_raise_option + assert_raise(I18n::MissingTranslationData) do + translate(:'translations.missing', :default => :'translations.missing_html', :raise => true) + end + end + def test_translate_with_two_defaults_named_html translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html']) assert_equal '<a>Hello World</a>', translation @@ -180,11 +181,26 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'A Generic String', translation end + def test_translate_with_object_default + translation = translate(:'translations.missing', default: 123) + assert_equal 123, translation + end + def test_translate_with_array_of_string_defaults translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string']) assert_equal 'A Generic String', translation end + def test_translate_with_array_of_defaults_with_nil + translation = translate(:'translations.missing', default: [:'also_missing', nil, 'A Generic String']) + assert_equal 'A Generic String', translation + end + + def test_translate_with_array_of_array_default + translation = translate(:'translations.missing', default: [[]]) + assert_equal [], translation + end + def test_translate_does_not_change_options options = {} translate(:'translations.missing', options) diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 0d6f31af9b..0e35c67516 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'minitest/mock' @@ -380,6 +379,11 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) end + def test_link_to_if_with_block + assert_equal "Fallback", link_to_if(false, "Showing", url_hash) { "Fallback" } + assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) { "Fallback" } + end + def request_for_url(url, opts = {}) env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts) ActionDispatch::Request.new(env) @@ -480,6 +484,11 @@ class UrlHelperTest < ActiveSupport::TestCase link_to_unless_current("Listing", "http://www.example.com/") end + def test_link_to_unless_with_block + assert_dom_equal %{<a href="/">Showing</a>}, link_to_unless(false, "Showing", url_hash) { "Fallback" } + assert_equal "Fallback", link_to_unless(true, "Listing", url_hash) { "Fallback" } + end + def test_mail_to assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com") assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson") @@ -496,6 +505,11 @@ class UrlHelperTest < ActiveSupport::TestCase %{<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>}, mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.", reply_to: "foo@bar.com") ) + + assert_dom_equal( + %{<a href="mailto:me@example.com?body=This%20is%20the%20body%20of%20the%20message.&subject=This%20is%20an%20example%20email">My email</a>}, + mail_to("me@example.com", "My email", cc: '', bcc: '', subject: "This is an example email", body: "This is the body of the message.") + ) end def test_mail_to_with_img @@ -624,13 +638,13 @@ class UrlHelperControllerTest < ActionController::TestCase end def test_named_route_url_shows_host_and_path - get :show_named_route, kind: 'url' + get :show_named_route, params: { kind: 'url' } assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route', @response.body end def test_named_route_path_shows_only_path - get :show_named_route, kind: 'path' + get :show_named_route, params: { kind: 'path' } assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body end @@ -646,7 +660,7 @@ class UrlHelperControllerTest < ActionController::TestCase end end - get :show_named_route, kind: 'url' + get :show_named_route, params: { kind: 'url' } assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body end @@ -661,11 +675,11 @@ class UrlHelperControllerTest < ActionController::TestCase end def test_recall_params_should_normalize_id - get :show, id: '123' + get :show, params: { id: '123' } assert_equal 302, @response.status assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location - get :show, name: '123' + get :show, params: { name: '123' } assert_equal 'ok', @response.body end @@ -704,7 +718,7 @@ class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase end def test_link_to_unless_current_shows_link - get :show, id: 1 + get :show, params: { id: 1 } assert_equal %{<a href="/tasks">tasks</a>\n} + %{<a href="#{@request.protocol}#{@request.host_with_port}/tasks">tasks</a>}, @response.body @@ -778,21 +792,21 @@ class PolymorphicControllerTest < ActionController::TestCase def test_existing_resource @controller = WorkshopsController.new - get :show, id: 1 + get :show, params: { id: 1 } assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body end def test_new_nested_resource @controller = SessionsController.new - get :index, workshop_id: 1 + get :index, params: { workshop_id: 1 } assert_equal %{/workshops/1/sessions\n<a href="/workshops/1/sessions">Session</a>}, @response.body end def test_existing_nested_resource @controller = SessionsController.new - get :show, workshop_id: 1, id: 1 + get :show, params: { workshop_id: 1, id: 1 } assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body end end diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index afdd42be33..c523723fc4 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,27 +1,97 @@ -* `ActiveJob::Base.deserialize` delegates to the job class +* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to + `ActiveJob::Base` as `provider_job_id`. + Fixes #18821. - Since `ActiveJob::Base#deserialize` can be overridden by subclasses (like - `ActiveJob::Base#serialize`) this allows jobs to attach arbitrary metadata - when they get serialized and read it back when they get performed. Example: + *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 + understand. + + *y-yagi* + +* A generated job now inherits from `app/jobs/application_job.rb` by default. + + *Jeroen van Baarsen* + +* Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on + type. + + This allows specific jobs to be tested, while preventing others from + being performed unnecessarily. - class DeliverWebhookJob < ActiveJob::Base - def serialize - super.merge('attempt_number' => (@attempt_number || 0) + 1) + Example: + + def test_hello_job + assert_performed_jobs 1, only: HelloJob do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later + end end - def deserialize(job_data) - super - @attempt_number = job_data['attempt_number'] + An array may also be specified, to support testing multiple jobs. + + Example: + + def test_hello_and_logging_jobs + assert_nothing_raised do + assert_performed_jobs 2, only: [HelloJob, LoggingJob] do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later('stewie') + RescueJob.perform_later('david') + end + end end - rescue_from(TimeoutError) do |exception| - raise exception if @attempt_number > 5 - retry_job(wait: 10) + Fixes #18802. + + *Michael Ryan* + +* Allow keyword arguments to be used with Active Job. + + Fixes #18741. + + *Sean Griffin* + +* Add `:only` option to `assert_enqueued_jobs`, to check the number of times + a specific kind of job is enqueued. + + Example: + + def test_logging_job + assert_enqueued_jobs 1, only: LoggingJob do + LoggingJob.perform_later + HelloJob.perform_later('jeremy') + end end - end - *Isaac Seymour* + *George Claghorn* + +* `ActiveJob::Base.deserialize` delegates to the job class. + + Since `ActiveJob::Base#deserialize` can be overridden by subclasses (like + `ActiveJob::Base#serialize`) this allows jobs to attach arbitrary metadata + when they get serialized and read it back when they get performed. + + Example: + + class DeliverWebhookJob < ActiveJob::Base + def serialize + super.merge('attempt_number' => (@attempt_number || 0) + 1) + end + + def deserialize(job_data) + super + @attempt_number = job_data['attempt_number'] + end + rescue_from(TimeoutError) do |exception| + raise exception if @attempt_number > 5 + retry_job(wait: 10) + end + end + + *Isaac Seymour* Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/README.md b/activejob/README.md index 8c83d3669a..f9a3183b1a 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -7,7 +7,7 @@ small units of work and run in parallel, really. It also serves as the backend for Action Mailer's #deliver_later functionality that makes it easy to turn any mailing into a job for running later. That's -one of the most common jobs in a modern web application: Sending emails outside +one of the most common jobs in a modern web application: sending emails outside of the request-response cycle, so the user doesn't have to wait on it. The main point is to ensure that all Rails apps will have a job infrastructure @@ -102,7 +102,7 @@ see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails. The latest version of Active Job can be installed with RubyGems: ``` - % [sudo] gem install activejob + % gem install activejob ``` Source code can be downloaded as part of the Rails project on GitHub @@ -118,7 +118,7 @@ Active Job is released under the MIT license: ## Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org diff --git a/activejob/Rakefile b/activejob/Rakefile index 1922f256ec..8c86df3c91 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION) @@ -28,14 +27,14 @@ namespace :test do end ACTIVEJOB_ADAPTERS.each do |adapter| - task("env:#{adapter}") { ENV['AJADAPTER'] = adapter } + task("env:#{adapter}") { ENV['AJ_ADAPTER'] = adapter } Rake::TestTask.new(adapter => "test:env:#{adapter}") do |t| t.description = "Run adapter tests for #{adapter}" t.libs << 'test' t.test_files = FileList['test/cases/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end @@ -54,7 +53,7 @@ namespace :test do t.libs << 'test' t.test_files = FileList['test/integration/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end @@ -74,17 +73,3 @@ def run_without_aborting(tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end - - -spec = eval(File.read('activejob.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc 'Release to rubygems' -task release: :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index 5404ece804..24e38e495f 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Job framework with pluggable queues.' s.description = 'Declare job classes that can be run by a variety of queueing backends.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 752be6898e..cdb37879b6 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -1,10 +1,12 @@ -require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash' module ActiveJob # Raised when an exception is raised during job arguments deserialization. # # Wraps the original exception raised as +original_exception+. class DeserializationError < StandardError + # The original exception that was raised during deserialization of job + # arguments. attr_reader :original_exception def initialize(e) #:nodoc: @@ -24,6 +26,7 @@ module ActiveJob module Arguments extend self + # :nodoc: TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ] # Serializes a set of arguments. Whitelisted types are returned @@ -43,8 +46,13 @@ module ActiveJob end private + # :nodoc: GLOBALID_KEY = '_aj_globalid'.freeze - private_constant :GLOBALID_KEY + # :nodoc: + SYMBOL_KEYS_KEY = '_aj_symbol_keys'.freeze + # :nodoc: + WITH_INDIFFERENT_ACCESS_KEY = '_aj_hash_with_indifferent_access'.freeze + private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY def serialize_argument(argument) case argument @@ -54,10 +62,15 @@ module ActiveJob { GLOBALID_KEY => argument.to_global_id.to_s } when Array argument.map { |arg| serialize_argument(arg) } + when ActiveSupport::HashWithIndifferentAccess + result = serialize_hash(argument) + result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true) + result when Hash - argument.each_with_object({}) do |(key, value), hash| - hash[serialize_hash_key(key)] = serialize_argument(value) - end + symbol_keys = argument.each_key.grep(Symbol).map(&:to_s) + result = serialize_hash(argument) + result[SYMBOL_KEYS_KEY] = symbol_keys + result else raise SerializationError.new("Unsupported argument type: #{argument.class.name}") end @@ -75,7 +88,7 @@ module ActiveJob if serialized_global_id?(argument) deserialize_global_id argument else - deserialize_hash argument + deserialize_hash(argument) end else raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" @@ -90,13 +103,28 @@ module ActiveJob GlobalID::Locator.locate hash[GLOBALID_KEY] end + def serialize_hash(argument) + argument.each_with_object({}) do |(key, value), hash| + hash[serialize_hash_key(key)] = serialize_argument(value) + end + end + def deserialize_hash(serialized_hash) - serialized_hash.each_with_object({}.with_indifferent_access) do |(key, value), hash| - hash[key] = deserialize_argument(value) + result = serialized_hash.transform_values { |v| deserialize_argument(v) } + if result.delete(WITH_INDIFFERENT_ACCESS_KEY) + result = result.with_indifferent_access + elsif symbol_keys = result.delete(SYMBOL_KEYS_KEY) + result = transform_symbol_keys(result, symbol_keys) end + result end - RESERVED_KEYS = [GLOBALID_KEY, GLOBALID_KEY.to_sym] + # :nodoc: + RESERVED_KEYS = [ + GLOBALID_KEY, GLOBALID_KEY.to_sym, + SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym, + WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, + ] private_constant :RESERVED_KEYS def serialize_hash_key(key) @@ -109,5 +137,15 @@ module ActiveJob raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}") end end + + def transform_symbol_keys(hash, symbol_keys) + hash.transform_keys do |key| + if symbol_keys.include?(key) + key.to_sym + else + key + end + end + end end end diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index ddd7d1361c..0528572cd0 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -1,4 +1,6 @@ module ActiveJob + # Provides general behavior that will be included into every Active Job + # object that inherits from ActiveJob::Base. module Core extend ActiveSupport::Concern @@ -15,6 +17,9 @@ module ActiveJob # Queue in which the job will reside. attr_writer :queue_name + + # ID optionally provided by adapter + attr_accessor :provider_job_id end # These methods will be included into any Active Job object, adding diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 430c17e1bf..98d92385dd 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -1,6 +1,7 @@ require 'active_job/arguments' module ActiveJob + # Provides behavior for enqueuing and retrying jobs. module Enqueuing extend ActiveSupport::Concern diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index cd29e6908e..54774db601 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -81,7 +81,7 @@ module ActiveJob private def queue_name(event) - event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" + event.payload[:adapter].class.name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" end def args_info(job) diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index d610d30e01..457015b741 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -1,35 +1,63 @@ require 'active_job/queue_adapters/inline_adapter' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' module ActiveJob # The <tt>ActiveJob::QueueAdapter</tt> module is used to load the - # correct adapter. The default queue adapter is the :inline queue. + # correct adapter. The default queue adapter is the +:inline+ queue. module QueueAdapter #:nodoc: extend ActiveSupport::Concern + included do + class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false + self.queue_adapter = :inline + end + # Includes the setter method for changing the active queue adapter. module ClassMethods - mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter } + # Returns the backend queue provider. The default queue adapter + # is the +:inline+ queue. See QueueAdapters for more information. + def queue_adapter + _queue_adapter + end # Specify the backend queue provider. The default queue adapter - # is the :inline queue. See QueueAdapters for more + # is the +:inline+ queue. See QueueAdapters for more # information. - def queue_adapter=(name_or_adapter) - @@queue_adapter = \ - case name_or_adapter - when :test - ActiveJob::QueueAdapters::TestAdapter.new - when Symbol, String - load_adapter(name_or_adapter) - else - name_or_adapter if name_or_adapter.respond_to?(:enqueue) - end + def queue_adapter=(name_or_adapter_or_class) + self._queue_adapter = interpret_adapter(name_or_adapter_or_class) end private - def load_adapter(name) - "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize + + def interpret_adapter(name_or_adapter_or_class) + case name_or_adapter_or_class + when Symbol, String + ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new + else + if queue_adapter?(name_or_adapter_or_class) + name_or_adapter_or_class + elsif queue_adapter_class?(name_or_adapter_or_class) + ActiveSupport::Deprecation.warn "Passing an adapter class is deprecated " \ + "and will be removed in Rails 5.1. Please pass an adapter name " \ + "(.queue_adapter = :#{name_or_adapter_or_class.name.demodulize.remove('Adapter').underscore}) " \ + "or an instance (.queue_adapter = #{name_or_adapter_or_class.name}.new) instead." + name_or_adapter_or_class.new + else + raise ArgumentError + end end + end + + QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze + + def queue_adapter?(object) + QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) } + end + + def queue_adapter_class?(object) + object.is_a?(Class) && QUEUE_ADAPTER_METHODS.all? { |meth| object.public_method_defined?(meth) } + end end end end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 4b91c93dbe..1335e3236e 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -27,13 +27,76 @@ module ActiveJob # | 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 | - # | Active Job | Yes | Yes | Yes | No | No | No | + # + # ==== Async + # + # Yes: The Queue Adapter runs the jobs in a separate or forked process. + # + # No: The job is run in the same process. + # + # ==== Queues + # + # Yes: Jobs may set which queue they are run in with queue_as or by using the set + # method. + # + # ==== Delayed + # + # Yes: The adapter will run the job in the future through perform_later. + # + # (Gem): An additional gem is required to use perform_later with this adapter. + # + # No: The adapter will run jobs at the next opportunity and cannot use perform_later. + # + # N/A: The adapter does not support queueing. # # NOTE: - # queue_classic does not support Job scheduling. However you can implement this - # yourself or you can use the queue_classic-later gem. See the documentation for - # ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic does not support job scheduling. + # However, you can use the queue_classic-later gem. + # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter. + # + # ==== Priorities + # + # The order in which jobs are processed can be configured differently depending + # on the adapter. + # + # Job: Any class inheriting from the adapter may set the priority on the job + # object relative to other jobs. + # + # Queue: The adapter can set the priority for job queues, when setting a queue + # with Active Job this will be respected. + # + # Yes: Allows the priority to be set on the job object, at the queue level or + # as default configuration option. # + # No: Does not allow the priority of jobs to be configured. + # + # N/A: The adapter does not support queueing, and therefore sorting them. + # + # ==== Timeout + # + # When a job will stop after the allotted time. + # + # Job: The timeout can be set for each instance of the job class. + # + # Queue: The timeout is set for all jobs on the queue. + # + # Global: The adapter is configured that all jobs have a maximum run time. + # + # N/A: This adapter does not run in a separate process, and therefore timeout + # is unsupported. + # + # ==== Retries + # + # Job: The number of retries can be set per instance of the job class. + # + # Yes: The Number of retries can be configured globally, for each instance or + # on the queue. This adapter may also present failed instances of the job class + # that can be restarted. + # + # Global: The adapter has a global number of retries. + # + # N/A: The adapter does not run in a separate process, and therefore doesn't + # support retries. module QueueAdapters extend ActiveSupport::Autoload @@ -48,5 +111,18 @@ module ActiveJob autoload :SneakersAdapter autoload :SuckerPunchAdapter autoload :TestAdapter + + ADAPTER = 'Adapter'.freeze + 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 + end end end diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb index 2453d065de..17703e3e41 100644 --- a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -13,15 +13,13 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :backburner class BackburnerAdapter - class << self - def enqueue(job) #:nodoc: - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name - end + def enqueue(job) #:nodoc: + Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name + end - def enqueue_at(job, timestamp) #:nodoc: - delay = timestamp - Time.current.to_f - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay - end + def enqueue_at(job, timestamp) #:nodoc: + delay = timestamp - Time.current.to_f + Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb index 69d9e70de3..ac83da2b9c 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -13,14 +13,16 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter - class << self - def enqueue(job) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) - end + def enqueue(job) #:nodoc: + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + job.provider_job_id = delayed_job.id + delayed_job + end - def enqueue_at(job, timestamp) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) - end + def enqueue_at(job, timestamp) #:nodoc: + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) + job.provider_job_id = delayed_job.id + delayed_job end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index e25d88e723..8ad5f4de07 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -9,14 +9,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :inline class InlineAdapter - class << self - def enqueue(job) #:nodoc: - Base.execute(job.serialize) - end + def enqueue(job) #:nodoc: + Base.execute(job.serialize) + end - def enqueue_at(*) #:nodoc: - raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html") - end + def enqueue_at(*) #:nodoc: + raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html" end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb index 30aa5a4670..0e198922fc 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -16,16 +16,18 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :qu class QuAdapter - class << self - def enqueue(job, *args) #:nodoc: - Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| - payload.instance_variable_set(:@queue, job.queue_name) - end.push - end + def enqueue(job, *args) #:nodoc: + qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| + payload.instance_variable_set(:@queue, job.queue_name) + end.push + + # qu_job can be nil depending on the configured backend + job.provider_job_id = qu_job.id unless qu_job.nil? + qu_job + end - def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp, *args) #:nodoc: + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper < Qu::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index e501fe0368..90947aa98d 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -15,14 +15,16 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :que class QueAdapter - class << self - def enqueue(job) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name - end + def enqueue(job) #:nodoc: + que_job = JobWrapper.enqueue job.serialize + job.provider_job_id = que_job.attrs["job_id"] + que_job + end - def enqueue_at(job, timestamp) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) - end + def enqueue_at(job, timestamp) #:nodoc: + que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp) + job.provider_job_id = que_job.attrs["job_id"] + que_job end class JobWrapper < Que::Job #:nodoc: 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 34c11a68b2..059754a87f 100644 --- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -17,29 +17,27 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :queue_classic class QueueClassicAdapter - class << self - def enqueue(job) #:nodoc: - build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) - end + def enqueue(job) #:nodoc: + build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + end - def enqueue_at(job, timestamp) #:nodoc: - queue = build_queue(job.queue_name) - unless queue.respond_to?(:enqueue_at) - raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \ - '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) + def enqueue_at(job, timestamp) #:nodoc: + queue = build_queue(job.queue_name) + unless queue.respond_to?(:enqueue_at) + raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \ + '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) + end - # Builds a <tt>QC::Queue</tt> object to schedule jobs on. - # - # If you have a custom <tt>QC::Queue</tt> subclass you'll need to subclass - # <tt>ActiveJob::QueueAdapters::QueueClassicAdapter</tt> and override the - # <tt>build_queue</tt> method. - def build_queue(queue_name) - QC::Queue.new(queue_name) - end + # Builds a <tt>QC::Queue</tt> object to schedule jobs on. + # + # If you have a custom <tt>QC::Queue</tt> subclass you'll need to subclass + # <tt>ActiveJob::QueueAdapters::QueueClassicAdapter</tt> and override the + # <tt>build_queue</tt> method. + def build_queue(queue_name) + QC::Queue.new(queue_name) end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb index 88c6b48fef..417854afd8 100644 --- a/activejob/lib/active_job/queue_adapters/resque_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb @@ -26,18 +26,16 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :resque class ResqueAdapter - class << self - def enqueue(job) #:nodoc: - Resque.enqueue_to job.queue_name, JobWrapper, job.serialize - end + def enqueue(job) #:nodoc: + Resque.enqueue_to job.queue_name, JobWrapper, job.serialize + end - def enqueue_at(job, timestamp) #:nodoc: - unless Resque.respond_to?(:enqueue_at_with_queue) - raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \ - "resque-scheduler gem. Please add it to your Gemfile and run bundle install" - end - Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.serialize + def enqueue_at(job, timestamp) #:nodoc: + unless Resque.respond_to?(:enqueue_at_with_queue) + raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \ + "resque-scheduler gem. Please add it to your Gemfile and run bundle install" end + Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.serialize end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index 21005fc728..c321776bf5 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -15,22 +15,22 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sidekiq class SidekiqAdapter - class << self - def enqueue(job) #:nodoc: - #Sidekiq::Client does not support symbols as keys - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ] - end + def enqueue(job) #:nodoc: + #Sidekiq::Client does not support symbols as keys + job.provider_job_id = Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ] + end - def enqueue_at(job, timestamp) #:nodoc: - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ], - 'at' => timestamp - end + def enqueue_at(job, timestamp) #:nodoc: + job.provider_job_id = Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ], + 'at' => timestamp end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index 6d60a2f303..f102c6567e 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -16,19 +16,19 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sneakers class SneakersAdapter - @monitor = Monitor.new + def initialize + @monitor = Monitor.new + end - class << self - def enqueue(job) #:nodoc: - @monitor.synchronize do - JobWrapper.from_queue job.queue_name - JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize) - end + def enqueue(job) #:nodoc: + @monitor.synchronize do + JobWrapper.from_queue job.queue_name + JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize) end + end - def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp) #:nodoc: + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb index be9e7fd03a..c6c35f8ab4 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -18,14 +18,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sucker_punch class SuckerPunchAdapter - class << self - def enqueue(job) #:nodoc: - JobWrapper.new.async.perform job.serialize - end + def enqueue(job) #:nodoc: + JobWrapper.new.async.perform job.serialize + end - def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp) #:nodoc: + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index ea9df9a063..9b7b7139f4 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -10,15 +10,9 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :test class TestAdapter - delegate :name, to: :class - attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs) + attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter) attr_writer(:enqueued_jobs, :performed_jobs) - def initialize - self.perform_enqueued_jobs = false - self.perform_enqueued_at_jobs = false - end - # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. def enqueued_jobs @enqueued_jobs ||= [] @@ -30,22 +24,37 @@ module ActiveJob end def enqueue(job) #:nodoc: - if perform_enqueued_jobs - performed_jobs << {job: job.class, args: job.serialize['arguments'], queue: job.queue_name} - Base.execute job.serialize - else - enqueued_jobs << {job: job.class, args: job.serialize['arguments'], queue: job.queue_name} - end + return if filtered?(job) + + job_data = job_to_hash(job) + enqueue_or_perform(perform_enqueued_jobs, job, job_data) end def enqueue_at(job, timestamp) #:nodoc: - if perform_enqueued_at_jobs - performed_jobs << {job: job.class, args: job.serialize['arguments'], queue: job.queue_name, at: timestamp} + return if filtered?(job) + + job_data = job_to_hash(job, at: timestamp) + enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) + end + + private + + def job_to_hash(job, extras = {}) + { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras) + end + + def enqueue_or_perform(perform, job, job_data) + if perform + performed_jobs << job_data Base.execute job.serialize else - enqueued_jobs << {job: job.class, args: job.serialize['arguments'], queue: job.queue_name, at: timestamp} + enqueued_jobs << job_data end end + + def filtered?(job) + filter && !Array(filter).include?(job.class) + end end end end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 2efcea7f2e..9b307e8dc8 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/subclasses' require 'active_support/core_ext/hash/keys' module ActiveJob @@ -7,8 +8,17 @@ module ActiveJob included do def before_setup - @old_queue_adapter = queue_adapter - ActiveJob::Base.queue_adapter = :test + test_adapter = ActiveJob::QueueAdapters::TestAdapter.new + + @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| + # only override explicitly set adapters, a quirk of `class_attribute` + klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) + end.map do |klass| + [klass, klass.queue_adapter].tap do + klass.queue_adapter = test_adapter + end + end + clear_enqueued_jobs clear_performed_jobs super @@ -16,7 +26,9 @@ module ActiveJob def after_teardown super - ActiveJob::Base.queue_adapter = @old_queue_adapter + @old_queue_adapters.each do |(klass, adapter)| + klass.queue_adapter = adapter + end end # Asserts that the number of enqueued jobs matches the given number. @@ -42,16 +54,24 @@ module ActiveJob # HelloJob.perform_later('rafael') # end # end - def assert_enqueued_jobs(number) + # + # The number of times a specific job is enqueued can be asserted. + # + # def test_logging_job + # assert_enqueued_jobs 2, only: LoggingJob do + # LoggingJob.perform_later + # HelloJob.perform_later('jeremy') + # end + # end + def assert_enqueued_jobs(number, only: nil) if block_given? - original_count = enqueued_jobs.size + original_count = enqueued_jobs_size(only: only) yield - new_count = enqueued_jobs.size - assert_equal original_count + number, new_count, - "#{number} jobs expected, but #{new_count - original_count} were enqueued" + new_count = enqueued_jobs_size(only: only) + assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" else - enqueued_jobs_size = enqueued_jobs.size - assert_equal number, enqueued_jobs_size, "#{number} jobs expected, but #{enqueued_jobs_size} were enqueued" + actual_count = enqueued_jobs_size(only: only) + assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" end end @@ -71,11 +91,19 @@ module ActiveJob # end # end # + # It can be asserted that no jobs of a specific kind are enqueued: + # + # def test_no_logging + # assert_no_enqueued_jobs only: LoggingJob do + # HelloJob.perform_later('jeremy') + # end + # end + # # Note: This assertion is simply a shortcut for: # # assert_enqueued_jobs 0, &block - def assert_no_enqueued_jobs(&block) - assert_enqueued_jobs 0, &block + def assert_no_enqueued_jobs(only: nil, &block) + assert_enqueued_jobs 0, only: only, &block end # Asserts that the number of performed jobs matches the given number. @@ -109,12 +137,34 @@ module ActiveJob # HelloJob.perform_later('sean') # end # end - def assert_performed_jobs(number) + # + # The block form supports filtering. If the :only option is specified, + # then only the listed job(s) will be performed. + # + # def test_hello_job + # assert_performed_jobs 1, only: HelloJob do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later + # end + # end + # + # An array may also be specified, to support testing multiple jobs. + # + # def test_hello_and_logging_jobs + # assert_nothing_raised do + # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later('stewie') + # RescueJob.perform_later('david') + # end + # end + # end + def assert_performed_jobs(number, only: nil) if block_given? original_count = performed_jobs.size - perform_enqueued_jobs { yield } + perform_enqueued_jobs(only: only) { yield } new_count = performed_jobs.size - assert_equal original_count + number, new_count, + assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were performed" else performed_jobs_size = performed_jobs.size @@ -141,11 +191,33 @@ module ActiveJob # end # end # + # The block form supports filtering. If the :only option is specified, + # then only the listed job(s) will be performed. + # + # def test_hello_job + # assert_performed_jobs 1, only: HelloJob do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later + # end + # end + # + # An array may also be specified, to support testing multiple jobs. + # + # def test_hello_and_logging_jobs + # assert_nothing_raised do + # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later('stewie') + # RescueJob.perform_later('david') + # end + # end + # end + # # Note: This assertion is simply a shortcut for: # # assert_performed_jobs 0, &block - def assert_no_performed_jobs(&block) - assert_performed_jobs 0, &block + def assert_no_performed_jobs(only: nil, &block) + assert_performed_jobs 0, only: only, &block end # Asserts that the job passed in the block has been enqueued with the given arguments. @@ -159,9 +231,10 @@ module ActiveJob original_enqueued_jobs = enqueued_jobs.dup clear_enqueued_jobs args.assert_valid_keys(:job, :args, :at, :queue) + serialized_args = serialize_args_for_assertion(args) yield matching_job = enqueued_jobs.any? do |job| - args.all? { |key, value| value == job[key] } + serialized_args.all? { |key, value| value == job[key] } end assert matching_job, "No enqueued job found with #{args}" ensure @@ -179,24 +252,31 @@ module ActiveJob original_performed_jobs = performed_jobs.dup clear_performed_jobs 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| - args.all? { |key, value| value == job[key] } + serialized_args.all? { |key, value| value == job[key] } end assert matching_job, "No performed job found with #{args}" ensure queue_adapter.performed_jobs = original_performed_jobs + performed_jobs end - def perform_enqueued_jobs - @old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs - @old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs - queue_adapter.perform_enqueued_jobs = true - queue_adapter.perform_enqueued_at_jobs = true - yield - ensure - queue_adapter.perform_enqueued_jobs = @old_perform_enqueued_jobs - queue_adapter.perform_enqueued_at_jobs = @old_perform_enqueued_at_jobs + def perform_enqueued_jobs(only: nil) + old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs + old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs + old_filter = queue_adapter.filter + + begin + queue_adapter.perform_enqueued_jobs = true + queue_adapter.perform_enqueued_at_jobs = true + queue_adapter.filter = only + yield + ensure + queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs + queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs + queue_adapter.filter = old_filter + end end def queue_adapter @@ -215,6 +295,22 @@ module ActiveJob def clear_performed_jobs performed_jobs.clear end + + def enqueued_jobs_size(only: nil) + if only + enqueued_jobs.select { |job| job.fetch(:job) == only }.size + else + enqueued_jobs.size + end + end + + def serialize_args_for_assertion(args) + serialized_args = args.dup + if job_args = serialized_args.delete(:args) + serialized_args[:args] = ActiveJob::Arguments.serialize(job_args) + end + serialized_args + end end end end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 979ffcb748..2115fb9f71 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -1,6 +1,6 @@ require 'rails/generators/named_base' -module Rails +module Rails # :nodoc: module Generators # :nodoc: class JobGenerator < Rails::Generators::NamedBase # :nodoc: desc 'This generator creates an active job file at app/jobs' @@ -18,7 +18,6 @@ module Rails def create_job_file template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb") end - end end end diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb index 462c71d917..4ad2914a45 100644 --- a/activejob/lib/rails/generators/job/templates/job.rb +++ b/activejob/lib/rails/generators/job/templates/job.rb @@ -1,5 +1,5 @@ <% module_namespacing do -%> -class <%= class_name %>Job < ActiveJob::Base +class <%= class_name %>Job < ApplicationJob queue_as :<%= options[:queue] %> def perform(*args) diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb index 6570c55a83..6d75ae9a7c 100644 --- a/activejob/test/cases/adapter_test.rb +++ b/activejob/test/cases/adapter_test.rb @@ -1,7 +1,7 @@ require 'helper' class AdapterTest < ActiveSupport::TestCase - test "should load #{ENV['AJADAPTER']} adapter" do - assert_equal "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify, ActiveJob::Base.queue_adapter.name + test "should load #{ENV['AJ_ADAPTER']} adapter" do + assert_equal "active_job/queue_adapters/#{ENV['AJ_ADAPTER']}_adapter".classify, ActiveJob::Base.queue_adapter.class.name end end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index dbe36fc572..8b9b62190f 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -2,6 +2,7 @@ require 'helper' require 'active_job/arguments' require 'models/person' require 'active_support/core_ext/hash/indifferent_access' +require 'jobs/kwargs_job' class ArgumentSerializationTest < ActiveSupport::TestCase setup do @@ -31,16 +32,26 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end test 'should convert records to Global IDs' do - assert_arguments_roundtrip [@person], ['_aj_globalid' => @person.to_gid.to_s] + assert_arguments_roundtrip [@person] end test 'should dive deep into arrays and hashes' do - assert_arguments_roundtrip [3, [@person]], [3, ['_aj_globalid' => @person.to_gid.to_s]] - assert_arguments_roundtrip [{ 'a' => @person }], [{ 'a' => { '_aj_globalid' => @person.to_gid.to_s }}.with_indifferent_access] + assert_arguments_roundtrip [3, [@person]] + assert_arguments_roundtrip [{ 'a' => @person }] end - test 'should stringify symbol hash keys' do - assert_equal [ 'a' => 1 ], ActiveJob::Arguments.serialize([ a: 1 ]) + test 'should maintain string and symbol keys' do + assert_arguments_roundtrip([a: 1, "b" => 2]) + end + + test 'should maintain hash with indifferent access' do + symbol_key = { a: 1 } + string_key = { 'a' => 1 } + indifferent_access = { a: 1 }.with_indifferent_access + + assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([symbol_key]).first + assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([string_key]).first + assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first end test 'should disallow non-string/symbol hash keys' do @@ -71,14 +82,22 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end end + test 'allows for keyword arguments' do + KwargsJob.perform_later(argument: 2) + + assert_equal "Job with argument: 2", JobBuffer.last_value + end + private def assert_arguments_unchanged(*args) - assert_arguments_roundtrip args, args + assert_arguments_roundtrip args + end + + def assert_arguments_roundtrip(args) + assert_equal args, perform_round_trip(args) end - def assert_arguments_roundtrip(args, expected_serialized_args) - serialized = ActiveJob::Arguments.serialize(args) - assert_equal expected_serialized_args, serialized - assert_equal args, ActiveJob::Arguments.deserialize(serialized) + def perform_round_trip(args) + ActiveJob::Arguments.deserialize(ActiveJob::Arguments.serialize(args)) end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 64aae00441..b18be553ec 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -6,7 +6,7 @@ require 'jobs/logging_job' require 'jobs/nested_job' require 'models/person' -class AdapterTest < ActiveSupport::TestCase +class LoggingTest < ActiveSupport::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb new file mode 100644 index 0000000000..fb3fdc392f --- /dev/null +++ b/activejob/test/cases/queue_adapter_test.rb @@ -0,0 +1,56 @@ +require 'helper' + +module ActiveJob + module QueueAdapters + class StubOneAdapter + def enqueue(*); end + def enqueue_at(*); end + end + + class StubTwoAdapter + def enqueue(*); end + def enqueue_at(*); end + end + end +end + +class QueueAdapterTest < ActiveJob::TestCase + test 'should forbid nonsense arguments' do + assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex } + assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex.new } + end + + test 'should warn on passing an adapter class' do + klass = Class.new do + def self.name + 'fake' + end + + def enqueue(*); end + def enqueue_at(*); end + end + + assert_deprecated { ActiveJob::Base.queue_adapter = klass } + end + + test 'should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling' do + base_queue_adapter = ActiveJob::Base.queue_adapter + + child_job_one = Class.new(ActiveJob::Base) + child_job_one.queue_adapter = :stub_one + + assert_not_equal ActiveJob::Base.queue_adapter, child_job_one.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter + + child_job_two = Class.new(ActiveJob::Base) + child_job_two.queue_adapter = :stub_two + + assert_kind_of ActiveJob::QueueAdapters::StubTwoAdapter, child_job_two.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter, "child_job_one's queue adapter should remain unchanged" + assert_equal base_queue_adapter, ActiveJob::Base.queue_adapter, "ActiveJob::Base's queue adapter should remain unchanged" + + child_job_three = Class.new(ActiveJob::Base) + + assert_not_nil child_job_three.queue_adapter + end +end diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb index 1d0fdbd22d..ee816e1dd5 100644 --- a/activejob/test/cases/test_case_test.rb +++ b/activejob/test/cases/test_case_test.rb @@ -4,11 +4,20 @@ require 'jobs/logging_job' require 'jobs/nested_job' class ActiveJobTestCaseTest < ActiveJob::TestCase + # this tests that this job class doesn't get its adapter set. + # that's the correct behavior since we don't want to break + # the `class_attribute` inheritance + class TestClassAttributeInheritanceJob < ActiveJob::Base + def self.queue_adapter=(*) + raise 'Attemping to break `class_attribute` inheritance, bad!' + end + end + def test_include_helper assert_includes self.class.ancestors, ActiveJob::TestHelper end def test_set_test_adapter - assert_instance_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 784ede3674..04c4c446e2 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/date' require 'jobs/hello_job' require 'jobs/logging_job' require 'jobs/nested_job' +require 'jobs/rescue_job' +require 'models/person' class EnqueuedJobsTest < ActiveJob::TestCase def test_assert_enqueued_jobs @@ -29,6 +31,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_enqueued_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_enqueued_jobs_with_no_block assert_nothing_raised do HelloJob.perform_later('rafael') @@ -87,6 +100,65 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_match(/0 .* but 1/, error.message) end + def test_assert_enqueued_jobs_with_only_option + assert_nothing_raised do + assert_enqueued_jobs 1, only: HelloJob do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later + end + end + end + + def test_assert_enqueued_jobs_with_only_option_and_none_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_jobs 1, only: HelloJob do + LoggingJob.perform_later + end + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_enqueued_jobs_with_only_option_and_too_few_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_jobs 5, only: HelloJob do + HelloJob.perform_later('jeremy') + 4.times { LoggingJob.perform_later } + end + end + + assert_match(/5 .* but 1/, error.message) + end + + def test_assert_enqueued_jobs_with_only_option_and_too_many_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_jobs 1, only: HelloJob do + 2.times { HelloJob.perform_later('jeremy') } + end + end + + assert_match(/1 .* but 2/, error.message) + end + + def test_assert_no_enqueued_jobs_with_only_option + assert_nothing_raised do + assert_no_enqueued_jobs only: HelloJob do + LoggingJob.perform_later + end + end + end + + def test_assert_no_enqueued_jobs_with_only_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_enqueued_jobs only: HelloJob do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later + end + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_enqueued_job assert_enqueued_with(job: LoggingJob, queue: 'default') do LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later @@ -116,9 +188,36 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end end + + def test_assert_enqueued_job_with_global_id_args + ricardo = Person.new(9) + assert_enqueued_with(job: HelloJob, args: [ricardo]) do + HelloJob.perform_later(ricardo) + end + end + + def test_assert_enqueued_job_failure_with_global_id_args + ricardo = Person.new(9) + wilma = Person.new(11) + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_with(job: HelloJob, args: [wilma]) do + HelloJob.perform_later(ricardo) + end + end + + assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message + end end class PerformedJobsTest < ActiveJob::TestCase + def test_performed_enqueue_jobs_with_only_option_doesnt_leak_outside_the_block + assert_equal nil, queue_adapter.filter + perform_enqueued_jobs only: HelloJob do + assert_equal HelloJob, queue_adapter.filter + end + assert_equal nil, queue_adapter.filter + end + def test_assert_performed_jobs assert_nothing_raised do assert_performed_jobs 1 do @@ -142,6 +241,17 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_performed_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_performed_jobs_with_no_block assert_nothing_raised do perform_enqueued_jobs do @@ -204,6 +314,83 @@ class PerformedJobsTest < ActiveJob::TestCase assert_match(/0 .* but 1/, error.message) end + def test_assert_performed_jobs_with_only_option + assert_nothing_raised do + assert_performed_jobs 1, only: HelloJob do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later + end + end + end + + def test_assert_performed_jobs_with_only_option_as_array + assert_nothing_raised do + assert_performed_jobs 2, only: [HelloJob, LoggingJob] do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later('stewie') + RescueJob.perform_later('david') + end + end + end + + def test_assert_performed_jobs_with_only_option_and_none_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, only: HelloJob do + LoggingJob.perform_later + end + end + + assert_match(/1 .* but 0/, error.message) + end + + def test_assert_performed_jobs_with_only_option_and_too_few_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 5, only: HelloJob do + HelloJob.perform_later('jeremy') + 4.times { LoggingJob.perform_later } + end + end + + assert_match(/5 .* but 1/, error.message) + end + + def test_assert_performed_jobs_with_only_option_and_too_many_sent + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_jobs 1, only: HelloJob do + 2.times { HelloJob.perform_later('jeremy') } + end + end + + assert_match(/1 .* but 2/, error.message) + end + + def test_assert_no_performed_jobs_with_only_option + assert_nothing_raised do + assert_no_performed_jobs only: HelloJob do + LoggingJob.perform_later + end + end + end + + def test_assert_no_performed_jobs_with_only_option_as_array + assert_nothing_raised do + assert_no_performed_jobs only: [HelloJob, RescueJob] do + LoggingJob.perform_later + end + end + end + + def test_assert_no_performed_jobs_with_only_option_failure + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_performed_jobs only: HelloJob do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later + end + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_performed_job assert_performed_with(job: NestedJob, queue: 'default') do NestedJob.perform_later @@ -223,4 +410,23 @@ class PerformedJobsTest < ActiveJob::TestCase end end end + + def test_assert_performed_job_with_global_id_args + ricardo = Person.new(9) + assert_performed_with(job: HelloJob, args: [ricardo]) do + HelloJob.perform_later(ricardo) + end + end + + def test_assert_performed_job_failure_with_global_id_args + ricardo = Person.new(9) + wilma = Person.new(11) + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: HelloJob, args: [wilma]) do + HelloJob.perform_later(ricardo) + end + end + + assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message + end end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index db5265d7b2..57907042d9 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -5,7 +5,7 @@ require 'support/job_buffer' GlobalID.app = 'aj' -@adapter = ENV['AJADAPTER'] || 'inline' +@adapter = ENV['AJ_ADAPTER'] || 'inline' if ENV['AJ_INTEGRATION_TESTS'] require 'support/integration/helper' @@ -14,5 +14,3 @@ else end require 'active_support/testing/autorun' - -ActiveSupport::TestCase.test_order = :random diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 38874b51a8..d345092dee 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -1,5 +1,6 @@ require 'helper' require 'jobs/logging_job' +require 'jobs/hello_job' require 'active_support/core_ext/numeric/time' class QueuingTest < ActiveSupport::TestCase @@ -10,7 +11,7 @@ class QueuingTest < ActiveSupport::TestCase end test 'should not run jobs queued on a non-listening queue' do - skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) + skip if adapter_is?(:inline, :sucker_punch, :que) old_queue = TestJob.queue_name begin @@ -23,6 +24,16 @@ class QueuingTest < ActiveSupport::TestCase end end + test 'should supply a wrapped class name to Sidekiq' do + skip unless adapter_is?(:sidekiq) + Sidekiq::Testing.fake! do + ::HelloJob.perform_later + hash = ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.jobs.first + assert_equal "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", hash['class'] + assert_equal "HelloJob", hash['wrapped'] + end + end + test 'should not run job enqueued in the future' do begin TestJob.set(wait: 10.minutes).perform_later @id @@ -35,7 +46,7 @@ class QueuingTest < ActiveSupport::TestCase test 'should run job enqueued in the future at the specified time' do begin - TestJob.set(wait: 3.seconds).perform_later @id + TestJob.set(wait: 5.seconds).perform_later @id wait_for_jobs_to_finish_for(2.seconds) assert_not job_executed wait_for_jobs_to_finish_for(10.seconds) @@ -44,4 +55,17 @@ class QueuingTest < ActiveSupport::TestCase skip end end + + test 'should supply a provider_job_id when available for immediate jobs' do + skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que) + test_job = TestJob.perform_later @id + refute test_job.provider_job_id.nil?, '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) + 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' + end end diff --git a/activejob/test/jobs/kwargs_job.rb b/activejob/test/jobs/kwargs_job.rb new file mode 100644 index 0000000000..2df17d15ae --- /dev/null +++ b/activejob/test/jobs/kwargs_job.rb @@ -0,0 +1,7 @@ +require_relative '../support/job_buffer' + +class KwargsJob < ActiveJob::Base + def perform(argument: 1) + JobBuffer.add("Job with argument: #{argument}") + end +end diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb index 3a5b66a057..256ddb3cf3 100644 --- a/activejob/test/support/integration/adapters/qu.rb +++ b/activejob/test/support/integration/adapters/qu.rb @@ -3,7 +3,7 @@ module QuJobsManager require 'qu-rails' require 'qu-redis' ActiveJob::Base.queue_adapter = :qu - ENV['REDISTOGO_URL'] = "tcp://127.0.0.1:6379/12" + ENV['REDISTOGO_URL'] = "redis://127.0.0.1:6379/12" backend = Qu::Backend::Redis.new backend.namespace = "active_jobs_int_test" Qu.backend = backend diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index ba7657a42a..0cd8952a28 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -2,6 +2,15 @@ module QueJobsManager def setup require 'sequel' ActiveJob::Base.queue_adapter = :que + Que.mode = :off + Que.worker_count = 1 + end + + def clear_jobs + Que.clear! + end + + def start_workers que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test' uri = URI.parse(que_url) user = uri.user||ENV['USER'] @@ -11,24 +20,17 @@ module QueJobsManager %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} Que.connection = Sequel.connect(que_url) Que.migrate! - Que.mode = :off - Que.worker_count = 1 - rescue Sequel::DatabaseConnectionError - puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" - exit - end - - def clear_jobs - Que.clear! - end - def start_workers @thread = Thread.new do loop do - Que::Job.work("integration_tests") + Que::Job.work sleep 0.5 end end + + rescue Sequel::DatabaseConnectionError + puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index 038473ccdc..29c04bf625 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -1,18 +1,9 @@ module QueueClassicJobsManager def setup ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test' + ENV['QC_RAILS_DATABASE'] = 'false' ENV['QC_LISTEN_TIME'] = "0.5" - uri = URI.parse(ENV['QC_DATABASE_URL']) - user = uri.user||ENV['USER'] - pass = uri.password - db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} ActiveJob::Base.queue_adapter = :queue_classic - QC::Setup.create - rescue PG::ConnectionBad - puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" - exit end def clear_jobs @@ -20,11 +11,24 @@ module QueueClassicJobsManager end def start_workers - QC::Conn.disconnect + uri = URI.parse(ENV['QC_DATABASE_URL']) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + QC::Setup.create + + QC.default_conn_adapter.disconnect + QC.default_conn_adapter = nil @pid = fork do worker = QC::Worker.new(q_name: 'integration_tests') worker.start end + + rescue PG::ConnectionBad + puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index 6ff18fb56a..4988cdb33f 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -1,6 +1,8 @@ -require 'sidekiq/cli' require 'sidekiq/api' +require 'sidekiq/testing' +Sidekiq::Testing.disable! + module SidekiqJobsManager def setup @@ -17,33 +19,71 @@ module SidekiqJobsManager end def start_workers - fork do - sidekiq = Sidekiq::CLI.instance + continue_read, continue_write = IO.pipe + death_read, death_write = IO.pipe + + @pid = fork do + continue_read.close + death_write.close + + # Celluloid & Sidekiq are not warning-clean :( + $VERBOSE = false + + $stdin.reopen('/dev/null') + $stdout.sync = true + $stderr.sync = true + logfile = Rails.root.join("log/sidekiq.log").to_s - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - sidekiq.parse([ "--require", Rails.root.to_s, - "--queue", "integration_tests", - "--logfile", logfile, - "--pidfile", pidfile, - "--environment", "test", - "--concurrency", "1", - "--timeout", "1", - "--daemon", - ]) + Sidekiq::Logging.initialize_logger(logfile) + + self_read, self_write = IO.pipe + trap "TERM" do + self_write.puts("TERM") + end + + Thread.new do + begin + death_read.read + rescue Exception + end + self_write.puts("TERM") + end + require 'celluloid' - require 'sidekiq/scheduled' + Celluloid.logger = nil + require 'sidekiq/launcher' + sidekiq = Sidekiq::Launcher.new({queues: ["integration_tests"], + environment: "test", + concurrency: 1, + timeout: 1, + }) Sidekiq.poll_interval = 0.5 Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 - sidekiq.run + begin + sidekiq.run + continue_write.puts "started" + while readable_io = IO.select([self_read]) + signal = readable_io.first[0].gets.strip + raise Interrupt if signal == "TERM" + end + rescue Interrupt + end + + sidekiq.stop + exit! end - sleep 1 + continue_write.close + death_read.close + @worker_lifeline = death_write + + raise "Failed to start worker" unless continue_read.gets == "started\n" end def stop_workers - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - Process.kill 'TERM', File.open(pidfile).read.to_i - FileUtils.rm_f pidfile - rescue + if @pid + Process.kill 'TERM', @pid + Process.wait @pid + end end def can_run? diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 65994d6a1c..09a68738ad 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -1,4 +1,4 @@ -if ENV['AJADAPTER'] == 'delayed_job' +if ENV['AJ_ADAPTER'] == 'delayed_job' generate "delayed_job:active_record", "--quiet" rake("db:migrate") end diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index 9bd45e09e8..8c2e5a86c2 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -1,4 +1,4 @@ -puts "*** rake aj:integration:#{ENV['AJADAPTER']} ***\n" +puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" ENV["RAILS_ENV"] = "test" ActiveJob::Base.queue_name_prefix = nil @@ -20,7 +20,7 @@ require 'rails/test_help' Rails.backtrace_cleaner.remove_silencers! require_relative 'test_case_helpers' -ActiveSupport::TestCase.send(:include, TestCaseHelpers) +ActiveSupport::TestCase.include(TestCaseHelpers) JobsManager.current_manager.start_workers diff --git a/activejob/test/support/integration/jobs_manager.rb b/activejob/test/support/integration/jobs_manager.rb index 4df34aaeb1..78d48e8d9a 100644 --- a/activejob/test/support/integration/jobs_manager.rb +++ b/activejob/test/support/integration/jobs_manager.rb @@ -3,7 +3,7 @@ class JobsManager attr :adapter_name def self.current_manager - @@managers[ENV['AJADAPTER']] ||= new(ENV['AJADAPTER']) + @@managers[ENV['AJ_ADAPTER']] ||= new(ENV['AJ_ADAPTER']) end def initialize(adapter_name) diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index ee2f6aebea..7e87ede275 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -5,7 +5,7 @@ module TestCaseHelpers extend ActiveSupport::Concern included do - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do clear_jobs @@ -27,8 +27,8 @@ module TestCaseHelpers jobs_manager.clear_jobs end - def adapter_is?(adapter) - ActiveJob::Base.queue_adapter.name.split("::").last.gsub(/Adapter$/, '').underscore==adapter.to_s + def adapter_is?(*adapter_class_symbols) + adapter_class_symbols.map(&:to_s).include?(ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore) end def wait_for_jobs_to_finish_for(seconds=60) diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb index 2e210acb6b..0232da1370 100644 --- a/activejob/test/support/que/inline.rb +++ b/activejob/test/support/que/inline.rb @@ -3,7 +3,11 @@ require 'que' Que::Job.class_eval do class << self; alias_method :original_enqueue, :enqueue; end def self.enqueue(*args) - args.pop if args.last.is_a?(Hash) + if args.last.is_a?(Hash) + options = args.pop + options.delete(:run_at) + args << options unless options.empty? + end self.run(*args) end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index f86b4804c8..dddfd940bb 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,89 @@ +* Ensure `method_missing` is called for methods passed to + `ActiveModel::Serialization#serializable_hash` that don't exist. + + *Jay Elaraj* + +* 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. + + It makes the dirty-attributes query methods consistent before and after + saving. + + *Fernando Tapia Rico* + +* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of + plain Ruby. + + *Sean Griffin* + +* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` + with no replacement. + + *Wojciech Wnętrzak* + +* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and + `ActiveModel::Errors#[]=` methods that have inconsistent behavior. + + *Wojciech Wnętrzak* + +* Allow symbol as values for `tokenize` of `LengthValidator`. + + *Kensuke Naito* + +* Assigning an unknown attribute key to an `ActiveModel` instance during initialization + will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of + `NoMethodError`. + + Example: + + User.new(foo: 'some value') + # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User. + + *Eugene Gilburg* + +* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` + allowing to use it for any object as an includable module. + + Example: + + class Cat + include ActiveModel::AttributeAssignment + attr_accessor :name, :status + end + + cat = Cat.new + cat.assign_attributes(name: "Gorby", status: "yawning") + cat.name # => 'Gorby' + cat.status # => 'yawning' + cat.assign_attributes(status: "sleeping") + cat.name # => 'Gorby' + cat.status # => 'sleeping' + + *Bogdan Gusiev* + +* Add `ActiveModel::Errors#details` + + To be able to return type of used validator, one can now call `details` + on errors instance. + + Example: + + class User < ActiveRecord::Base + validates :name, presence: true + end + + user = User.new; user.valid?; user.errors.details + => {name: [{error: :blank}]} + + *Wojciech Wnętrzak* + +* Change validates_acceptance_of to accept true by default. + + The default for validates_acceptance_of is now "1" and true. + In the past, only "1" was the default and you were required to add + accept: true. + * Remove deprecated `ActiveModel::Dirty#reset_#{attribute}` and `ActiveModel::Dirty#reset_changes`. diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index f6beff14e1..5c36b1277e 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -49,7 +49,7 @@ behavior out of the box: send("#{attr}=", nil) end end - + person = Person.new person.clear_name person.clear_age @@ -132,7 +132,7 @@ behavior out of the box: "Name" end end - + person = Person.new person.name = nil person.validate! @@ -216,10 +216,10 @@ behavior out of the box: {Learn more}[link:classes/ActiveModel/Validations.html] * Custom validators - + class HasNameValidator < ActiveModel::Validator def validate(record) - record.errors[:name] = "must exist" if record.name.blank? + record.errors.add(:name, "must exist") if record.name.blank? end end @@ -242,7 +242,7 @@ behavior out of the box: The latest version of Active Model can be installed with RubyGems: - % [sudo] gem install activemodel + % gem install activemodel Source code can be downloaded as part of the Rails project on GitHub diff --git a/activemodel/Rakefile b/activemodel/Rakefile index c30a559ef5..5a67f0a151 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,12 +1,12 @@ -dir = File.dirname(__FILE__) - require 'rake/testtask' +dir = File.dirname(__FILE__) + task :default => :test Rake::TestTask.new do |t| t.libs << "test" - t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort + t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -19,18 +19,3 @@ namespace :test do end or raise "Failures" end end - -require 'rubygems/package_task' - -spec = eval(File.read("#{dir}/activemodel.gemspec")) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 3c6eb56296..8d00b3aa27 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'A toolkit for building modeling frameworks (part of Rails).' s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' 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 46d60db756..8aa1b6f664 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -28,6 +28,7 @@ require 'active_model/version' module ActiveModel extend ActiveSupport::Autoload + autoload :AttributeAssignment autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks @@ -49,6 +50,7 @@ module ActiveModel eager_autoload do autoload :Errors autoload :StrictValidationFailed, 'active_model/errors' + autoload :UnknownAttributeError, 'active_model/errors' end module Serializers diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb new file mode 100644 index 0000000000..087d11f708 --- /dev/null +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -0,0 +1,52 @@ +require 'active_support/core_ext/hash/keys' + +module ActiveModel + module AttributeAssignment + include ActiveModel::ForbiddenAttributesProtection + + # Allows you to set all the attributes by passing in a hash of attributes with + # keys matching the attribute names. + # + # If the passed hash responds to <tt>permitted?</tt> method and the return value + # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> + # exception is raised. + # + # class Cat + # include ActiveModel::AttributeAssignment + # attr_accessor :name, :status + # end + # + # cat = Cat.new + # cat.assign_attributes(name: "Gorby", status: "yawning") + # cat.name # => 'Gorby' + # cat.status => 'yawning' + # cat.assign_attributes(status: "sleeping") + # cat.name # => 'Gorby' + # cat.status => 'sleeping' + def assign_attributes(new_attributes) + if !new_attributes.respond_to?(:stringify_keys) + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + end + return if new_attributes.blank? + + attributes = new_attributes.stringify_keys + _assign_attributes(sanitize_for_mass_assignment(attributes)) + end + + private + + def _assign_attributes(attributes) + attributes.each do |k, v| + _assign_attribute(k, v) + end + end + + def _assign_attribute(k, v) + if respond_to?("#{k}=") + public_send("#{k}=", v) + else + raise UnknownAttributeError.new(self, k) + end + end + end +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 96be551264..286cd6c206 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -23,7 +23,7 @@ module ActiveModel # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your class. - # * Call each of its method you want to add, such as +attribute_method_suffix+ + # * Call each of its methods you want to add, such as +attribute_method_suffix+ # or +attribute_method_prefix+. # * Call +define_attribute_methods+ after the other methods are called. # * Define the various generic +_attribute+ methods that you have declared. @@ -225,9 +225,9 @@ 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 + # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix # methods, or they will not hook in. # @@ -239,7 +239,7 @@ module ActiveModel # # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_methods :name, :age, :address # # private @@ -253,9 +253,9 @@ 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 + # 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 # method, or they will not hook in. # @@ -267,7 +267,7 @@ module ActiveModel # # # Call to define_attribute_method must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_method :name # # private @@ -363,7 +363,7 @@ module ActiveModel end # Define a method `name` in `mod` that dispatches to `send` - # using the given `extra` args. This fallbacks `define_method` + # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: defn = if name =~ NAME_COMPILABLE_REGEXP @@ -419,7 +419,7 @@ module ActiveModel # returned by <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use - # the attributes hash -- except for multiple assigns with + # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 6214802074..2cf39b68fb 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -49,7 +49,7 @@ module ActiveModel # puts 'block successfully called.' # end # - # You can choose not to have all three callbacks by passing a hash to the + # You can choose to have only specific callbacks by passing a hash to the # +define_model_callbacks+ method. # # define_model_callbacks :create, only: [:after, :before] diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index afba9bab0d..0169c20e0b 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -52,10 +52,10 @@ module ActiveModel # end # end # - # A newly instantiated object is unchanged: + # A newly instantiated +Person+ object is unchanged: # - # person = Person.find_by(name: 'Uncle Bob') - # person.changed? # => false + # person = Person.new + # person.changed? # => false # # Change the name: # @@ -71,54 +71,57 @@ module ActiveModel # Save the changes: # # person.save - # person.changed? # => false - # person.name_changed? # => false + # person.changed? # => false + # person.name_changed? # => false # # Reset the changes: # - # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.name_previously_changed? # => true + # person.name_previous_change # => ["Uncle Bob", "Bill"] # person.reload! - # person.previous_changes # => {} + # person.previous_changes # => {} # # Rollback the changes: # # person.name = "Uncle Bob" # person.rollback! - # person.name # => "Bill" - # person.name_changed? # => false + # person.name # => "Bill" + # person.name_changed? # => false # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' - # person.name_changed? # => false - # person.name_change # => nil + # person.name_changed? # => false + # person.name_change # => nil # # Which attributes have changed? # # person.name = 'Bob' - # person.changed # => ["name"] - # person.changes # => {"name" => ["Bill", "Bob"]} + # person.changed # => ["name"] + # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of - # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing. # Otherwise \Active \Model can't track changes to in-place attributes. Note # that Active Record can detect in-place modifications automatically. You do - # not need to call +[attribute_name]_will_change!+ on Active Record models. + # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models. # # person.name_will_change! - # person.name_change # => ["Bill", "Bill"] + # person.name_change # => ["Bill", "Bill"] # person.name << 'y' - # person.name_change # => ["Bill", "Billy"] + # person.name_change # => ["Bill", "Billy"] module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + attribute_method_suffix '_previously_changed?', '_previous_change' attribute_method_affix prefix: 'restore_', suffix: '!' end - # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. + # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise. # # person.changed? # => false # person.name = 'bob' @@ -166,7 +169,7 @@ module ActiveModel @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new end - # Handle <tt>*_changed?</tt> for +method_missing+. + # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, options = {}) #:nodoc: result = changes_include?(attr) result &&= options[:to] == __send__(attr) if options.key?(:to) @@ -174,11 +177,16 @@ module ActiveModel result end - # Handle <tt>*_was</tt> for +method_missing+. + # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Handles <tt>*_previously_changed?</tt> for +method_missing+. + def attribute_previously_changed?(attr, options = {}) #:nodoc: + previous_changes_include?(attr) + end + # Restore all previous data of the provided attributes. def restore_attributes(attributes = changed) attributes.each { |attr| restore_attribute! attr } @@ -186,9 +194,17 @@ module ActiveModel private + # Returns +true+ if attr_name is changed, +false+ otherwise. def changes_include?(attr_name) attributes_changed_by_setter.include?(attr_name) end + alias attribute_changed_by_setter? changes_include? + + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + @previously_changed.include?(attr_name) + end # Removes current changes and makes them accessible through +previous_changes+. def changes_applied # :doc: @@ -196,18 +212,23 @@ module ActiveModel @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Clear all dirty data: current changes and previous changes. + # Clears all dirty data: current changes and previous changes. def clear_changes_information # :doc: @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Handle <tt>*_change</tt> for +method_missing+. + # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end - # Handle <tt>*_will_change!</tt> for +method_missing+. + # Handles <tt>*_previous_change</tt> for +method_missing+. + def attribute_previous_change(attr) + @previously_changed[attr] if attribute_previously_changed?(attr) + end + + # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) @@ -220,7 +241,7 @@ module ActiveModel set_attribute_was(attr, value) end - # Handle <tt>restore_*!</tt> for +method_missing+. + # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) @@ -229,7 +250,7 @@ module ActiveModel end # This is necessary because `changed_attributes` might be overridden in - # other implemntations (e.g. in `ActiveRecord`) + # other implementations (e.g. in `ActiveRecord`) alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: # Force an attribute to have a particular "before" value diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 55687cb3c7..29e0c977ce 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -2,6 +2,8 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/string/filters' module ActiveModel # == Active \Model \Errors @@ -23,7 +25,7 @@ module ActiveModel # attr_reader :errors # # def validate! - # errors.add(:name, "cannot be nil") if name.nil? + # errors.add(:name, :blank, message: "cannot be nil") if name.nil? # end # # # The following methods are needed to be minimally implemented @@ -32,20 +34,20 @@ module ActiveModel # send(attr) # end # - # def Person.human_attribute_name(attr, options = {}) + # def self.human_attribute_name(attr, options = {}) # attr # end # - # def Person.lookup_ancestors + # def self.lookup_ancestors # [self] # 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: @@ -58,8 +60,9 @@ module ActiveModel include Enumerable CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] + MESSAGE_OPTIONS = [:message] - attr_reader :messages + attr_reader :messages, :details # Pass in the instance of the object that is using the errors object. # @@ -70,11 +73,13 @@ module ActiveModel # end def initialize(base) @base = base - @messages = {} + @messages = Hash.new { |messages, attribute| messages[attribute] = [] } + @details = Hash.new { |details, attribute| details[attribute] = [] } end def initialize_dup(other) # :nodoc: @messages = other.messages.dup + @details = other.details.deep_dup super end @@ -85,6 +90,7 @@ module ActiveModel # person.errors.full_messages # => [] def clear messages.clear + details.clear end # Returns +true+ if the error messages include an error for the given key @@ -96,35 +102,46 @@ module ActiveModel def include?(attribute) messages[attribute].present? end - # aliases include? alias :has_key? :include? - # aliases include? alias :key? :include? # Get messages for +key+. # # person.errors.messages # => {:name=>["cannot be nil"]} # person.errors.get(:name) # => ["cannot be nil"] - # person.errors.get(:age) # => nil + # person.errors.get(:age) # => [] def get(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1. + + To achieve the same use model.errors[:#{key}]. + MESSAGE + messages[key] end # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) - # person.errors.get(:name) # => ["can't be nil"] + # person.errors[:name] # => ["can't be nil"] def set(key, value) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. + + Use model.errors.add(:#{key}, #{value.inspect}) instead. + MESSAGE + messages[key] = value end # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => nil + # person.errors[:name] # => [] def delete(key) + details.delete(key) messages.delete(key) end @@ -134,7 +151,7 @@ module ActiveModel # person.errors[:name] # => ["cannot be nil"] # person.errors['name'] # => ["cannot be nil"] def [](attribute) - get(attribute.to_sym) || set(attribute.to_sym, []) + messages[attribute.to_sym] end # Adds to the supplied attribute the supplied error message. @@ -142,38 +159,45 @@ module ActiveModel # person.errors[:name] = "must be set" # person.errors[:name] # => ['must be set'] def []=(attribute, error) - self[attribute] << error + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1. + + Use model.errors.add(:#{attribute}, #{error.inspect}) instead. + MESSAGE + + messages[attribute.to_sym] << error end # Iterates through each error key, value pair in the error messages hash. # Yields the attribute and the error for that attribute. If the attribute # has more than one error message, yields once for each error message. # - # person.errors.add(:name, "can't be blank") + # person.errors.add(:name, :blank, message: "can't be blank") # person.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # - # person.errors.add(:name, "must be specified") + # person.errors.add(:name, :not_specified, message: "must be specified") # person.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end def each messages.each_key do |attribute| - self[attribute].each { |error| yield attribute, error } + messages[attribute].each { |error| yield attribute, error } end end # Returns the number of error messages. # - # person.errors.add(:name, "can't be blank") + # person.errors.add(:name, :blank, message: "can't be blank") # person.errors.size # => 1 - # person.errors.add(:name, "must be specified") + # person.errors.add(:name, :not_specified, message: "must be specified") # person.errors.size # => 2 def size values.flatten.size end + alias :count :size # Returns all message values. # @@ -191,40 +215,20 @@ module ActiveModel messages.keys end - # Returns an array of error messages, with the attribute name included. - # - # person.errors.add(:name, "can't be blank") - # person.errors.add(:name, "must be specified") - # person.errors.to_a # => ["name can't be blank", "name must be specified"] - def to_a - full_messages - end - - # Returns the number of error messages. - # - # person.errors.add(:name, "can't be blank") - # person.errors.count # => 1 - # person.errors.add(:name, "must be specified") - # person.errors.count # => 2 - def count - to_a.size - end - # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? - all? { |k, v| v && v.empty? && !v.is_a?(String) } + size.zero? end - # aliases empty? - alias_method :blank?, :empty? + alias :blank? :empty? # Returns an xml formatted representation of the Errors hash. # - # person.errors.add(:name, "can't be blank") - # person.errors.add(:name, "must be specified") + # person.errors.add(:name, :blank, message: "can't be blank") + # person.errors.add(:name, :not_specified, message: "must be specified") # person.errors.to_xml # # => # # <?xml version=\"1.0\" encoding=\"UTF-8\"?> @@ -261,17 +265,20 @@ module ActiveModel end end - # Adds +message+ to the error messages on +attribute+. More than one error - # can be added to the same +attribute+. If no +message+ is supplied, - # <tt>:invalid</tt> is assumed. + # Adds +message+ to the error messages and used validator type to +details+ on +attribute+. + # More than one error can be added to the same +attribute+. + # If no +message+ is supplied, <tt>:invalid</tt> is assumed. # # person.errors.add(:name) # # => ["is invalid"] - # person.errors.add(:name, 'must be implemented') + # person.errors.add(:name, :not_implemented, message: "must be implemented") # # => ["is invalid", "must be implemented"] # # person.errors.messages - # # => {:name=>["must be implemented", "is invalid"]} + # # => {:name=>["is invalid", "must be implemented"]} + # + # person.errors.details + # # => {:name=>[{error: :not_implemented}, {error: :invalid}]} # # If +message+ is a symbol, it will be translated using the appropriate # scope (see +generate_message+). @@ -293,17 +300,23 @@ module ActiveModel # +attribute+ should be set to <tt>:base</tt> if the error is not # directly associated with a single attribute. # - # person.errors.add(:base, "either name or email must be present") + # person.errors.add(:base, :name_or_email_blank, + # message: "either name or email must be present") # person.errors.messages # # => {:base=>["either name or email must be present"]} + # person.errors.details + # # => {:base=>[{error: :name_or_email_blank}]} def add(attribute, message = :invalid, options = {}) + message = message.call if message.respond_to?(:call) + detail = normalize_detail(attribute, message, options) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true raise exception, full_message(attribute, message) end - self[attribute] << message + details[attribute.to_sym] << detail + messages[attribute.to_sym] << message end # Will add an error message to each of the attributes in +attributes+ @@ -313,6 +326,14 @@ module ActiveModel # person.errors.messages # # => {:name=>["can't be empty"]} def add_on_empty(attributes, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1 + + To achieve the same use: + + errors.add(attribute, :empty, options) if value.nil? || value.empty? + MESSAGE + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false @@ -327,6 +348,14 @@ module ActiveModel # person.errors.messages # # => {:name=>["can't be blank"]} def add_on_blank(attributes, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1 + + To achieve the same use: + + errors.add(attribute, :empty, options) if value.blank? + MESSAGE + Array(attributes).each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) add(attribute, :blank, options) if value.blank? @@ -339,6 +368,7 @@ module ActiveModel # person.errors.add :name, :blank # person.errors.added? :name, :blank # => true def added?(attribute, message = :invalid, options = {}) + message = message.call if message.respond_to?(:call) message = normalize_message(attribute, message, options) self[attribute].include? message end @@ -356,6 +386,7 @@ module ActiveModel def full_messages map { |attribute, message| full_message(attribute, message) } end + alias :to_a :full_messages # Returns all the full error messages for a given attribute in an array. # @@ -368,7 +399,7 @@ module ActiveModel # person.errors.full_messages_for(:name) # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"] def full_messages_for(attribute) - (get(attribute) || []).map { |message| full_message(attribute, message) } + messages[attribute].map { |message| full_message(attribute, message) } end # Returns a full message for a given attribute. @@ -388,8 +419,8 @@ module ActiveModel # Translates an error message in its default scope # (<tt>activemodel.errors.messages</tt>). # - # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, - # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if + # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, + # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if # that is not there also, it returns the translation of the default message # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model # name, translated attribute name and the value are available for @@ -421,7 +452,6 @@ module ActiveModel defaults = [] end - defaults << options.delete(:message) defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" @@ -430,6 +460,7 @@ module ActiveModel defaults.flatten! key = defaults.shift + defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { @@ -447,12 +478,14 @@ module ActiveModel case message when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - when Proc - message.call else message end end + + def normalize_detail(attribute, message, options) + { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) + end end # Raised when a validation cannot be corrected by end users and are considered @@ -472,4 +505,15 @@ module ActiveModel # # => ActiveModel::StrictValidationFailed: Name can't be blank class StrictValidationFailed < StandardError end + + # Raised when unknown attributes are supplied via mass assignment. + class UnknownAttributeError < NoMethodError + attr_reader :record, :attribute + + def initialize(record, attribute) + @record = record + @attribute = attribute + super("unknown attribute '#{attribute}' for #{@record.class}.") + end + end end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 38087521a2..010eaeb170 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -21,28 +21,27 @@ module ActiveModel # +self+. module Tests - # == Responds to <tt>to_key</tt> + # Passes if the object's model responds to <tt>to_key</tt> and if calling + # this method returns +nil+ when the object is not persisted. + # Fails otherwise. # - # Returns an Enumerable of all (primary) key attributes - # or nil if <tt>model.persisted?</tt> is false. This is used by - # <tt>dom_id</tt> to generate unique ids for the object. + # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes + # of the model, and is used to a generate unique DOM id for the object. def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end - # == Responds to <tt>to_param</tt> - # - # Returns a string representing the object's key suitable for use in URLs - # or +nil+ if <tt>model.persisted?</tt> is +false+. + # Passes if the object's model responds to <tt>to_param</tt> and if + # calling this method returns +nil+ when the object is not persisted. + # Fails otherwise. # + # <tt>to_param</tt> is used to represent the object's key in URLs. # Implementers can decide to either raise an exception or provide a # default in case the record uses a composite primary key. There are no # tests for this behavior in lint because it doesn't make sense to force # any of the possible implementation strategies on the implementer. - # However, if the resource is not persisted?, then <tt>to_param</tt> - # should always return +nil+. def test_to_param assert model.respond_to?(:to_param), "The model should respond to to_param" def model.to_key() [1] end @@ -50,32 +49,34 @@ module ActiveModel assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end - # == Responds to <tt>to_partial_path</tt> + # Passes if the object's model responds to <tt>to_partial_path</tt> and if + # calling this method returns a string. Fails otherwise. # - # Returns a string giving a relative path. This is used for looking up - # partials. For example, a BlogPost model might return "blog_posts/blog_post" + # <tt>to_partial_path</tt> is used for looking up partials. For example, + # a BlogPost model might return "blog_posts/blog_post". def test_to_partial_path assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" assert_kind_of String, model.to_partial_path end - # == Responds to <tt>persisted?</tt> + # Passes if the object's model responds to <tt>persisted?</tt> and if + # calling this method returns either +true+ or +false+. Fails otherwise. # - # Returns a boolean that specifies whether the object has been persisted - # yet. This is used when calculating the URL for an object. If the object - # is not persisted, a form for that object, for instance, will route to - # the create action. If it is persisted, a form for the object will routes - # to the update action. + # <tt>persisted?</tt> is used when calculating the URL for an object. + # If the object is not persisted, a form for that object, for instance, + # will route to the create action. If it is persisted, a form for the + # object will route to the update action. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" assert_boolean model.persisted?, "persisted?" end - # == \Naming + # Passes if the object's model responds to <tt>model_name</tt> both as + # an instance method and as a class method, and if calling this method + # returns a string with some convenience methods: <tt>:human</tt>, + # <tt>:singular</tt> and <tt>:plural</tt>. # - # Model.model_name and Model#model_name must return a string with some - # convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and - # <tt>:plural</tt>. Check ActiveModel::Naming for more information. + # Check ActiveModel::Naming for more information. def test_model_naming assert model.class.respond_to?(:model_name), "The model class should respond to model_name" model_name = model.class.model_name @@ -88,12 +89,15 @@ module ActiveModel assert_equal model.model_name, model.class.model_name end - # == \Errors Testing + # Passes if the object's model responds to <tt>errors</tt> and if calling + # <tt>[](attribute)</tt> on the result of this method returns an array. + # Fails otherwise. # - # Returns an object that implements [](attribute) defined which returns an - # Array of Strings that are the errors for the attribute in question. - # If localization is used, the Strings should be localized for the current - # locale. If no error is present, this method should return an empty Array. + # <tt>errors[attribute]</tt> is used to retrieve the errors of a model + # for a given attribute. If errors are present, the method should return + # an array of strings that are the errors for the attribute in question. + # If localization is used, the strings should be localized for the current + # locale. If no error is present, the method should return an empty array. def test_errors_aref assert model.respond_to?(:errors), "The model should respond to errors" assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index bf07945fe1..061e35dd1e 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -6,6 +6,7 @@ en: # The values :model, :attribute and :value are always available for interpolation # The value :count is available when applicable. Can be used for pluralization. messages: + model_invalid: "Validation failed: %{errors}" inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" @@ -16,7 +17,7 @@ en: present: "must be blank" too_long: one: "is too long (maximum is 1 character)" - other: "is too long (maximum is %{count} characters)" + other: "is too long (maximum is %{count} characters)" too_short: one: "is too short (minimum is 1 character)" other: "is too short (minimum is %{count} characters)" diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index d51d6ddcc9..dac8d549a7 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -57,6 +57,7 @@ module ActiveModel # (see below). module Model extend ActiveSupport::Concern + include ActiveModel::AttributeAssignment include ActiveModel::Validations include ActiveModel::Conversion @@ -75,10 +76,8 @@ module ActiveModel # person = Person.new(name: 'bob', age: '18') # person.name # => "bob" # person.age # => "18" - def initialize(params={}) - params.each do |attr, value| - self.public_send("#{attr}=", value) - end if params + def initialize(attributes={}) + assign_attributes(attributes) if attributes super() end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index ada1f9a4f3..1f1749af4e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' +require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/delegation' module ActiveModel class Name @@ -129,7 +131,7 @@ module ActiveModel # # Equivalent to +to_s+. delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, - :to_str, to: :name + :to_str, :as_json, to: :name # Returns a new ActiveModel::Name instance. By default, the +namespace+ # and +name+ option will take the namespace and name of the given class diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 96e88f1b6c..89da74efa8 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -26,7 +26,7 @@ module ActiveModel # it). When this attribute has a +nil+ value, the validation will not be # triggered. # - # For further customizability, it is possible to supress the default + # For further customizability, it is possible to suppress the default # validations by passing <tt>validations: false</tt> as an argument. # # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password: @@ -77,13 +77,6 @@ module ActiveModel validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED validates_confirmation_of :password, allow_blank: true end - - # This code is necessary as long as the protected_attributes gem is supported. - if respond_to?(:attributes_protected_by_default) - def self.attributes_protected_by_default #:nodoc: - super + ['password_digest'] - end - end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 976f50b13e..f95849eb84 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -94,6 +94,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 ||= {} @@ -107,7 +138,7 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) } serializable_add_includes(options) do |association, records, opts| hash[association.to_s] = if records.respond_to?(:to_ary) diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 6a2668b8f7..c1019169e1 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -87,6 +87,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end + # :nodoc: VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # Adds a validation method or block to the class. This is useful when @@ -371,6 +372,15 @@ module ActiveModel !valid?(context) end + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +ValidationError+ otherwise. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_validation_error + end + # Hook method defining how an attribute value should be retrieved. By default # this is assumed to be an instance named after the attribute. Override this # method in subclasses should you need to retrieve the value for a given @@ -392,9 +402,33 @@ module ActiveModel protected def run_validations! #:nodoc: - _run_validate_callbacks + run_callbacks :validate errors.empty? end + + def raise_validation_error + raise(ValidationError.new(self)) + end + end + + # = Active Model ValidationError + # + # Raised by <tt>validate!</tt> when the model is invalid. Use the + # +model+ method to retrieve the record which did not validate. + # + # begin + # complex_operation_that_internally_calls_validate! + # rescue ActiveModel::ValidationError => invalid + # puts invalid.model.errors + # end + class ValidationError < StandardError + attr_reader :model + + def initialize(model) + @model = model + errors = @model.errors.full_messages.join(", ") + super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid")) + end end end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index ac5e79859b..ee160fb483 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -3,12 +3,12 @@ module ActiveModel module Validations class AcceptanceValidator < EachValidator # :nodoc: def initialize(options) - super({ allow_nil: true, accept: "1" }.merge!(options)) + super({ allow_nil: true, accept: ["1", true] }.merge!(options)) setup!(options[:class]) end def validate_each(record, attribute, value) - unless value == options[:accept] + unless acceptable_option?(value) record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil)) end end @@ -20,6 +20,10 @@ module ActiveModel klass.send(:attr_reader, *attr_readers) klass.send(:attr_writer, *attr_writers) end + + def acceptable_option?(value) + Array(options[:accept]).include?(value) + end end module HelperMethods diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..b4301c23e4 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_validation_callbacks { super } + run_callbacks(:validation) { super } end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 02478dd5b6..46a2e54fba 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -77,7 +77,7 @@ module ActiveModel # with: ->(person) { person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i } # end # - # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the + # Note: use <tt>\A</tt> and <tt>\z</tt> to match the start and end of the # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index a96b30cadd..c22a58f9e1 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/strip" + module ActiveModel # == Active \Model Length Validator @@ -18,6 +20,27 @@ module ActiveModel options[:minimum] = 1 end + if options[:tokenizer] + ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) + The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. + You can achieve the same functionality by defining an instance method + with the value that you want to validate the length of. For example, + + validates_length_of :essay, minimum: 100, + tokenizer: ->(str) { str.scan(/\w+/) } + + should be written as + + validates_length_of :words_in_essay, minimum: 100 + + private + + def words_in_essay + essay.scan(/\w+/) + end + EOS + end + super end @@ -38,7 +61,7 @@ module ActiveModel end def validate_each(record, attribute, value) - value = tokenize(value) + value = tokenize(record, value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length errors_options = options.except(*RESERVED_OPTIONS) @@ -59,10 +82,14 @@ module ActiveModel end private - - def tokenize(value) - if options[:tokenizer] && value.kind_of?(String) - options[:tokenizer].call(value) + def tokenize(record, value) + tokenizer = options[:tokenizer] + if tokenizer && value.kind_of?(String) + if tokenizer.kind_of?(Proc) + tokenizer.call(value) + elsif record.respond_to?(tokenizer) + record.send(tokenizer, value) + end end || value end @@ -84,8 +111,13 @@ module ActiveModel # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', - # tokenizer: ->(str) { str.scan(/\w+/) } + # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' + # + # private + # + # def words_in_essay + # essay.scan(/\w+/) + # end # end # # Configuration options: @@ -108,10 +140,6 @@ module ActiveModel # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. - # (e.g. <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words - # as in above example). Defaults to <tt>->(value) { value.split(//) }</tt> - # which counts individual characters. # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index ac32750946..1d2888a818 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -15,7 +15,7 @@ module ActiveModel # class MyValidator < ActiveModel::Validator # def validate(record) # if some_complex_logic - # record.errors[:base] = "This record is invalid" + # record.errors.add(:base, "This record is invalid") # end # end # @@ -163,6 +163,10 @@ module ActiveModel # +ArgumentError+ when invalid options are supplied. def check_validity! end + + def should_validate?(record) # :nodoc: + !record.persisted? || record.changed? || record.marked_for_destruction? + end end # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb new file mode 100644 index 0000000000..64a85e01eb --- /dev/null +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -0,0 +1,107 @@ +require "cases/helper" +require "active_support/hash_with_indifferent_access" + +class AttributeAssignmentTest < ActiveModel::TestCase + class Model + include ActiveModel::AttributeAssignment + + attr_accessor :name, :description + + def initialize(attributes = {}) + assign_attributes(attributes) + end + + def broken_attribute=(value) + raise ErrorFromAttributeWriter + end + + protected + + attr_writer :metadata + end + + class ErrorFromAttributeWriter < StandardError + end + + class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + def permit! + @permitted = true + end + + def permitted? + @permitted ||= false + end + + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, permitted? + end + end + end + + test "simple assignment" do + model = Model.new + + model.assign_attributes(name: "hello", description: "world") + assert_equal "hello", model.name + assert_equal "world", model.description + end + + test "assign non-existing attribute" do + model = Model.new + error = assert_raises(ActiveModel::UnknownAttributeError) do + model.assign_attributes(hz: 1) + end + + assert_equal model, error.record + assert_equal "hz", error.attribute + end + + test "assign private attribute" do + model = Model.new + assert_raises(ActiveModel::UnknownAttributeError) do + model.assign_attributes(metadata: { a: 1 }) + end + end + + test "does not swallow errors raised in an attribute writer" do + assert_raises(ErrorFromAttributeWriter) do + Model.new(broken_attribute: 1) + end + end + + test "an ArgumentError is raised if a non-hash-like obejct is passed" do + assert_raises(ArgumentError) do + Model.new(1) + end + end + + test "forbidden attributes cannot be used for mass assignment" do + params = ProtectedParams.new(name: "Guille", description: "m") + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Model.new(params) + end + end + + test "permitted attributes can be used for mass assignment" do + params = ProtectedParams.new(name: "Guille", description: "desc") + params.permit! + model = Model.new(params) + + assert_equal "Guille", model.name + assert_equal "desc", model.description + end + + test "regular hash should still be used for mass assignment" do + model = Model.new(name: "Guille", description: "m") + + assert_equal "Guille", model.name + assert_equal "m", model.description + end + + test "assigning no attributes should not raise, even if the hash is un-permitted" do + model = Model.new + assert_nil model.assign_attributes(ProtectedParams.new({})) + end +end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 8ffd62fd86..d17a12ad12 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -45,10 +45,6 @@ class DirtyTest < ActiveModel::TestCase def reload clear_changes_information end - - def deprecated_reload - reset_changes - end end setup do @@ -141,6 +137,19 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "setting new attributes should not affect previous changes" do + @model.name = "Jericho Cane" + @model.save + @model.name = "DudeFella ManGuy" + assert_equal [nil, "Jericho Cane"], @model.name_previous_change + end + + test "saving should preserve model's previous changed status" do + @model.name = "Jericho Cane" + @model.save + assert @model.name_previously_changed? + end + test "previous value is preserved when changed after save" do assert_equal({}, @model.changed_attributes) @model.name = "Paul" diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index efedd9055f..3fa63917d0 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -27,30 +27,38 @@ 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' + errors[:foo] << 'omg' errors.delete(:foo) assert_empty errors[:foo] end def test_include? errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' assert errors.include?(:foo), 'errors should include :foo' end def test_dup errors = ActiveModel::Errors.new(self) - errors[:foo] = 'bar' + errors[:foo] << 'bar' errors_dup = errors.dup - errors_dup[:bar] = 'omg' + errors_dup[:bar] << 'omg' assert_not_same errors_dup.messages, errors.messages end def test_has_key? errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' assert_equal true, errors.has_key?(:foo), 'errors should have key :foo' end @@ -61,7 +69,7 @@ class ErrorsTest < ActiveModel::TestCase def test_key? errors = ActiveModel::Errors.new(self) - errors[:foo] = 'omg' + errors[:foo] << 'omg' assert_equal true, errors.key?(:foo), 'errors should have key :foo' end @@ -81,37 +89,41 @@ class ErrorsTest < ActiveModel::TestCase test "get returns the errors for the provided key" do errors = ActiveModel::Errors.new(self) - errors[:foo] = "omg" + errors[:foo] << "omg" - assert_equal ["omg"], errors.get(:foo) + assert_deprecated do + assert_equal ["omg"], errors.get(:foo) + end end test "sets the error with the provided key" do errors = ActiveModel::Errors.new(self) - errors.set(:foo, "omg") + assert_deprecated do + errors.set(:foo, "omg") + end assert_equal({ foo: "omg" }, errors.messages) end test "error access is indifferent" do errors = ActiveModel::Errors.new(self) - errors[:foo] = "omg" + errors[:foo] << "omg" assert_equal ["omg"], errors["foo"] end test "values returns an array of messages" do errors = ActiveModel::Errors.new(self) - errors.set(:foo, "omg") - errors.set(:baz, "zomg") + errors.messages[:foo] = "omg" + errors.messages[:baz] = "zomg" assert_equal ["omg", "zomg"], errors.values end test "keys returns the error keys" do errors = ActiveModel::Errors.new(self) - errors.set(:foo, "omg") - errors.set(:baz, "zomg") + errors.messages[:foo] << "omg" + errors.messages[:baz] << "zomg" assert_equal [:foo, :baz], errors.keys end @@ -133,7 +145,9 @@ class ErrorsTest < ActiveModel::TestCase test "assign error" do person = Person.new - person.errors[:name] = 'should not be nil' + assert_deprecated do + person.errors[:name] = 'should not be nil' + end assert_equal ["should not be nil"], person.errors[:name] end @@ -143,6 +157,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["cannot be blank"], person.errors[:name] end + test "add an error message on a specific attribute with a defined type" do + person = Person.new + person.errors.add(:name, :blank, message: "cannot be blank") + assert_equal ["cannot be blank"], person.errors[:name] + end + test "add an error with a symbol" do person = Person.new person.errors.add(:name, :blank) @@ -206,6 +226,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.size end + test "count calculates the number of error messages" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_equal 1, person.errors.count + end + test "to_a returns the list of errors with complete messages containing the attribute names" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -281,46 +307,122 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, {}) - person.errors.add_on_empty :name + @mock_generator.expect(:call, nil, [:name, :empty, {}]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_empty :name + end + end end test "add_on_empty generates message for multiple attributes" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, {}) - person.errors.expects(:generate_message).with(:age, :empty, {}) - person.errors.add_on_empty [:name, :age] + @mock_generator.expect(:call, nil, [:name, :empty, {}]) + @mock_generator.expect(:call, nil, [:age, :empty, {}]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_empty [:name, :age] + end + end end test "add_on_empty generates message with custom default message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, { message: 'custom' }) - person.errors.add_on_empty :name, message: 'custom' + @mock_generator.expect(:call, nil, [:name, :empty, { message: 'custom' }]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_empty :name, message: 'custom' + end + end end test "add_on_empty generates message with empty string value" do person = Person.new person.name = '' - person.errors.expects(:generate_message).with(:name, :empty, {}) - person.errors.add_on_empty :name + @mock_generator.expect(:call, nil, [:name, :empty, {}]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_empty :name + end + end end test "add_on_blank generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, {}) - person.errors.add_on_blank :name + @mock_generator.expect(:call, nil, [:name, :blank, {}]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_blank :name + end + end end test "add_on_blank generates message for multiple attributes" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, {}) - person.errors.expects(:generate_message).with(:age, :blank, {}) - person.errors.add_on_blank [:name, :age] + @mock_generator.expect(:call, nil, [:name, :blank, {}]) + @mock_generator.expect(:call, nil, [:age, :blank, {}]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_blank [:name, :age] + end + end end test "add_on_blank generates message with custom default message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, { message: 'custom' }) - person.errors.add_on_blank :name, message: 'custom' + @mock_generator.expect(:call, nil, [:name, :blank, { message: 'custom' }]) + person.errors.stub(:generate_message, @mock_generator) do + assert_deprecated do + person.errors.add_on_blank :name, message: 'custom' + end + end + end + + test "details returns added error detail" do + person = Person.new + person.errors.add(:name, :invalid) + assert_equal({ name: [{ error: :invalid }] }, person.errors.details) + end + + test "details returns added error detail with custom option" do + person = Person.new + person.errors.add(:name, :greater_than, count: 5) + assert_equal({ name: [{ error: :greater_than, count: 5 }] }, person.errors.details) + end + + test "details do not include message option" do + person = Person.new + person.errors.add(:name, :invalid, message: "is bad") + assert_equal({ name: [{ error: :invalid }] }, person.errors.details) + end + + test "dup duplicates details" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + errors_dup = errors.dup + errors_dup.add(:name, :taken) + assert_not_equal errors_dup.details, errors.details + end + + test "delete removes details on given attribute" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + errors.delete(:name) + assert_empty errors.details[:name] + end + + test "delete returns the deleted messages" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + assert_equal ["is invalid"], errors.delete(:name) + end + + test "clear removes details" do + person = Person.new + person.errors.add(:name, :invalid) + + assert_equal 1, person.errors.details.count + person.errors.clear + assert person.errors.details.empty? end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4ce6103593..0d179ea9ad 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -12,9 +12,13 @@ I18n.enforce_available_locales = false require 'active_support/testing/autorun' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted +# Skips the current run on Rubinius using Minitest::Assertions#skip +def rubinius_skip(message = '') + skip message if RUBY_ENGINE == 'rbx' +end +# Skips the current run on JRuby using Minitest::Assertions#skip +def jruby_skip(message = '') + skip message if defined?(JRUBY_VERSION) +end diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb index ee0fa26546..3017f3541b 100644 --- a/activemodel/test/cases/model_test.rb +++ b/activemodel/test/cases/model_test.rb @@ -70,6 +70,8 @@ class ModelTest < ActiveModel::TestCase end def test_mixin_initializer_when_args_dont_exist - assert_raises(NoMethodError) { SimpleModel.new(hello: 'world') } + assert_raises(ActiveModel::UnknownAttributeError) do + SimpleModel.new(hello: 'world') + end end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 4ae41aa19c..8d3165cd78 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -16,6 +16,14 @@ class SerializationTest < ActiveModel::TestCase instance_values.except("address", "friends") end + def method_missing(method_name, *args) + if method_name == :bar + 'i_am_bar' + else + super + end + end + def foo 'i_am_foo' end @@ -58,23 +66,22 @@ class SerializationTest < ActiveModel::TestCase end def test_method_serializable_hash_should_work_with_methods_option - expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:foo]) + expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar", "email"=>"david@example.com"} + assert_equal expected, @user.serializable_hash(methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_only_and_methods - expected = {"foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(only: [], methods: [:foo]) + expected = {"foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(only: [], methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_except_and_methods - expected = {"gender"=>"male", "foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo]) + expected = {"gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo, :bar]) end - def test_should_not_call_methods_that_dont_respond - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:bar]) + def test_should_raise_NoMethodError_for_non_existing_method + assert_raise(NoMethodError) { @user.serializable_hash(methods: [:nada]) } end def test_should_use_read_attribute_for_serialization diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index e2eb91eeb0..d765a47636 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -195,4 +195,8 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"awesome":}, json assert_no_match %r{"preferences":}, json end + + test "Class.model_name should be json encodable" do + assert_match %r{"Contact"}, Contact.model_name.to_json + end end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 22fca5bd17..37faf6cef8 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -2,6 +2,7 @@ require 'cases/helper' require 'models/contact' require 'active_support/core_ext/object/instance_variables' require 'ostruct' +require 'yaml' module Admin class Contact < ::Contact diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index ebfe1cf4e4..9cbc77dfb5 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' require 'models/person' diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index e78aa1adaf..9c2114d83d 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' @@ -65,4 +64,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase ensure Person.clear_validators! end + + def test_validates_acceptance_of_true + Topic.validates_acceptance_of(:terms_of_service) + + assert Topic.new(terms_of_service: true).valid? + end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index 4b0dd58efb..75eb18e795 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' class Dog @@ -50,6 +49,14 @@ class DogWithMissingName < Dog validates_presence_of :name end +class DogValidatorWithOnCondition < Dog + before_validation :set_before_validation_marker, on: :create + after_validation :set_after_validation_marker, on: :create + + def set_before_validation_marker; self.history << 'before_validation_marker'; end + def set_after_validation_marker; self.history << 'after_validation_marker' ; end +end + class DogValidatorWithIfCondition < Dog before_validation :set_before_validation_marker1, if: -> { true } before_validation :set_before_validation_marker2, if: -> { false } @@ -73,6 +80,24 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal ["before_validation_marker1", "after_validation_marker1"], d.history end + def test_on_condition_is_respected_for_validation_with_matching_context + d = DogValidatorWithOnCondition.new + d.valid?(:create) + assert_equal ["before_validation_marker", "after_validation_marker"], d.history + end + + def test_on_condition_is_respected_for_validation_without_matching_context + d = DogValidatorWithOnCondition.new + d.valid?(:save) + assert_equal [], d.history + end + + def test_on_condition_is_respected_for_validation_without_context + d = DogValidatorWithOnCondition.new + d.valid? + assert_equal [], d.history + end + def test_before_validation_and_after_validation_callbacks_should_be_called d = DogWithMethodCallbacks.new d.valid? diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 1261937b56..296d3b4407 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index 65a2a1eb49..c1431548f7 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index 1ce41f9bc9..b269c3691a 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 0f91b73cd7..86bbbe6ebe 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 3eeb80a48b..da63df9152 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -62,7 +62,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase assert_equal 'custom message', @person.errors.generate_message(:title, :empty, message: 'custom message') end - # add_on_blank: generate_message(attr, :blank, message: custom_message) + # validates_presence_of: generate_message(attr, :blank, message: custom_message) def test_generate_message_blank_with_default_message assert_equal "can't be blank", @person.errors.generate_message(:title, :blank) end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 96084a32ba..70b93a202b 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - require "cases/helper" require 'models/person' @@ -13,6 +11,7 @@ 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 @@ -20,6 +19,7 @@ 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 +32,10 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, 'not found') - Person.expects(:human_attribute_name).with(:name, default: 'Name').returns("Person's name") - assert_equal ["Person's name not found"], @person.errors.full_messages + @mock_generator.expect(:call, "Person's name", [:name, default: 'Name']) + Person.stub(:human_attribute_name, @mock_generator) do + assert_equal ["Person's name not found"], @person.errors.full_messages + end end def test_errors_full_messages_uses_format @@ -62,8 +64,10 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')) - @person.valid? + @mock_generator.expect(:call, nil, [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -72,8 +76,10 @@ class I18nValidationTest < ActiveModel::TestCase 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) - @person.errors.expects(:generate_message).with(:title, :accepted, generate_message_options) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :accepted, generate_message_options]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -82,18 +88,22 @@ class I18nValidationTest < ActiveModel::TestCase 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 - @person.errors.expects(:generate_message).with(:title, :blank, generate_message_options) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :blank, generate_message_options]) + @person.errors.stub(:generate_message, @mock_generator) 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) - @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(count: 3)) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :too_short, generate_message_options.merge(count: 3)]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -103,8 +113,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(count: 5)) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :too_long, generate_message_options.merge(count: 5)]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -113,8 +125,10 @@ class I18nValidationTest < ActiveModel::TestCase 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) - @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(count: 5)) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :wrong_length, generate_message_options.merge(count: 5)]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -124,8 +138,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(value: '72x')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :invalid, generate_message_options.merge(value: '72x')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -135,8 +151,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -146,8 +164,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -157,8 +177,10 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_exclusion_of generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -168,8 +190,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -179,8 +203,10 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_numericality_of generated message #{name}" do Person.validates_numericality_of :title, validation_options @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(value: 'a')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :not_a_number, generate_message_options.merge(value: 'a')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -190,8 +216,10 @@ class I18nValidationTest < ActiveModel::TestCase 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' - @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(value: '0.0')) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :not_an_integer, generate_message_options.merge(value: '0.0')]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -201,8 +229,10 @@ class I18nValidationTest < ActiveModel::TestCase 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 - @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(value: 0)) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :odd, generate_message_options.merge(value: 0)]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end @@ -212,8 +242,10 @@ class I18nValidationTest < ActiveModel::TestCase 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 - @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(value: 1, count: 0)) - @person.valid? + @mock_generator.expect(:call, nil, [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]) + @person.errors.stub(:generate_message, @mock_generator) do + @person.valid? + end end end diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 3a8f3080e1..55d1fb4dcb 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'active_support/all' diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 046ffcb16f..ee901b75fb 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' @@ -320,8 +319,33 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: lambda {|str| str.scan(/\w+/) } + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: lambda {|str| str.scan(/\w+/) }, + ) + end + t = Topic.new(content: "this content should be long enough") + assert t.valid? + + t.content = "not long enough" + assert t.invalid? + assert t.errors[:content].any? + assert_equal ["Your essay must be at least 5 words."], t.errors[:content] + end + + + def test_validates_length_of_with_symbol + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: :my_word_tokenizer, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 12a22f9c40..05432abaff 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index ecf16d1e16..59b9db0795 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 8d4b74ee49..04101f3545 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/person' require 'models/topic' diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 005bf118c6..150dce379f 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 736c2deea8..9ee8b79da9 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' @@ -98,12 +97,14 @@ class ValidatesWithTest < ActiveModel::TestCase test "passes all configuration options to the validator class" do topic = Topic.new - validator = mock() - validator.expects(:new).with(foo: :bar, if: "1 == 1", class: Topic).returns(validator) - validator.expects(:validate).with(topic) + 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]) Topic.validates_with(validator, if: "1 == 1", foo: :bar) assert topic.valid? + validator.verify end test "validates_with with options" do diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 2b932683ea..f0317ad219 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/topic' @@ -352,6 +351,25 @@ class ValidationsTest < ActiveModel::TestCase assert_not_empty topic.errors end + def test_validate_with_bang + Topic.validates :title, presence: true + + assert_raise(ActiveModel::ValidationError) do + Topic.new.validate! + end + end + + def test_validate_with_bang_and_context + Topic.validates :title, presence: true, on: :context + + assert_raise(ActiveModel::ValidationError) do + Topic.new.validate!(:context) + end + + t = Topic.new(title: "Valid title") + assert t.validate!(:context) + end + def test_strict_validation_in_validates Topic.validates :title, strict: true, presence: true assert_raises ActiveModel::StrictValidationFailed do diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index 1411a093e9..fed50bc361 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -37,4 +37,8 @@ class Topic errors.add attr, "is missing" unless send(attr) end + def my_word_tokenizer(str) + str.scan(/\w+/) + end + end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 97e65a5bb9..d389d7f6fe 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,631 @@ +* 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 `:enum_prefix`/`:enum_suffix` option to `enum` definition. + + Fixes #17511 and #17415 + + *Igor Kapkov* + +* Correctly handle decimal arrays with defaults in the schema dumper. + + Fixes #20515. + + *Sean Griffin & jmondo* + +* Deprecate the PG `: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. + + ``` + # 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: + + create_table :foo do |t| + t.string :string_nocase, collation: 'NOCASE' + t.text :text_rtrim, collation: 'RTRIM' + end + + add_column :foo, :title, :string, collation: 'RTRIM' + + change_column :foo, :title, :string, collation: 'NOCASE' + + *Akshay Vishnoi* + +* Allow the use of symbols or strings to specify enum values in test + fixtures: + + awdr: + title: "Agile Web Development with Rails" + status: :proposed + + *George Claghorn* + +* Clear query cache when `ActiveRecord::Base#reload` is called. + + *Shane Hender, Pierre Nespo* + +* Include stored procedures and function on the MySQL structure dump. + + *Jonathan Worek* + +* Pass `:extend` option for `has_and_belongs_to_many` associations to the underlying `has_many :through`. + + *Jaehyun Shin* + +* Deprecate `Relation#uniq` use `Relation#distinct` instead. + + See #9683. + + *Yves Senn* + +* Allow single table inheritance instantiation to work when storing + demodulized class names. + + *Alex Robbin* + +* Correctly pass MySQL options when using `structure_dump` or + `structure_load`. + + Specifically, it fixes an issue when using SSL authentication. + + *Alex Coomans* + +* Dump indexes in `create_table` instead of `add_index`. + + If the adapter supports indexes in `create_table`, generated SQL is + slightly more efficient. + + *Ryuta Kamizono* + +* Correctly dump `:options` on `create_table` for MySQL. + + *Ryuta Kamizono* + +* PostgreSQL: `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_en, collation: 'en_US.UTF-8' + t.text :text_ja, collation: 'ja_JP.UTF-8' + end + + *Ryuta Kamizono* + +* Make `unscope` aware of "less than" and "greater than" conditions. + + *TAKAHASHI Kazuaki* + +* `find_by` and `find_by!` raise `ArgumentError` when called without + arguments. + + *Kohei Suzuki* + +* Revert behavior of `db:schema:load` back to loading the full + environment. This ensures that initializers are run. + + Fixes #19545. + + *Yves Senn* + +* Fix missing index when using `timestamps` with the `index` option. + + The `index` option used with `timestamps` should be passed to both + `column` definitions for `created_at` and `updated_at` rather than just + the first. + + *Paul Mucur* + +* Rename `:class` to `:anonymous_class` in association options. + + Fixes #19659. + + *Andrew White* + +* Autosave existing records on a has many through association when the parent + is new. + + Fixes #19782. + + *Sean Griffin* + +* Fixed a bug where uniqueness validations would error on out of range values, + even if an validation should have prevented it from hitting the database. + + *Andrey Voronkov* + +* MySQL: `:charset` and `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' + t.text :text_ascii, charset: 'ascii' + end + + *Ryuta Kamizono* + +* Foreign key related methods in the migration DSL respect + `ActiveRecord::Base.pluralize_table_names = false`. + + Fixes #19643. + + *Mehmet Emin İNAÇ* + +* Reduce memory usage from loading types on PostgreSQL. + + Fixes #19578. + + *Sean Griffin* + +* Add `config.active_record.warn_on_records_fetched_greater_than` option. + + When set to an integer, a warning will be logged whenever a result set + larger than the specified size is returned by a query. + + Fixes #16463. + + *Jason Nochlin* + +* Ignore `.psqlrc` when loading database structure. + + *Jason Weathered* + +* Fix referencing wrong table aliases while joining tables of has many through + association (only when calling calculation methods). + + Fixes #19276. + + *pinglamb* + +* Correctly persist a serialized attribute that has been returned to + its default value by an in-place modification. + + Fixes #19467. + + *Matthew Draper* + +* Fix generating the schema file when using PostgreSQL `BigInt[]` data type. + Previously the `limit: 8` was not coming through, and this caused it to + become `Int[]` data type after rebuilding from the schema. + + Fixes #19420. + + *Jake Waller* + +* Reuse the `CollectionAssociation#reader` cache when the foreign key is + available prior to save. + + *Ben Woosley* + +* Add `config.active_record.dump_schemas` to fix `db:structure:dump` + when using schema_search_path and PostgreSQL extensions. + + Fixes #17157. + + *Ryan Wallace* + +* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity. + + Fixes #18864. + + *Brandon Weiss* + +* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the + pg gem are known to have problems with Ruby 2.2. + + *Matt Brictson* + +* Correctly dump `serial` and `bigserial`. + + *Ryuta Kamizono* + +* Fix default `format` value in `ActiveRecord::Tasks::DatabaseTasks#schema_file`. + + *James Cox* + +* Don't enroll records in the transaction if they don't have commit callbacks. + This was causing a memory leak when creating many records inside a transaction. + + Fixes #15549. + + *Will Bryant*, *Aaron Patterson* + +* Correctly create through records when created on a has many through + association when using `where`. + + Fixes #19073. + + *Sean Griffin* + +* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL. + + *Ryuta Kamizono* + +* PostgreSQL no longer disables user triggers if system triggers can't be + disabled. Disabling user triggers does not fulfill what the method promises. + Rails currently requires superuser privileges for this method. + + If you absolutely rely on this behavior, consider patching + `disable_referential_integrity`. + + *Yves Senn* + +* Restore aborted transaction state when `disable_referential_integrity` fails + due to missing permissions. + + *Toby Ovod-Everett*, *Yves Senn* + +* In PostgreSQL, print a warning message if `disable_referential_integrity` + fails due to missing permissions. + + *Andrey Nering*, *Yves Senn* + +* Allow a `:limit` option for MySQL bigint primary key support. + + Example: + + create_table :foos, id: :primary_key, limit: 8 do |t| + end + + # or + + create_table :foos, id: false do |t| + t.primary_key :id, limit: 8 + end + + *Ryuta Kamizono* + +* `belongs_to` will now trigger a validation error by default if the association is not present. + You can turn this off on a per-association basis with `optional: true`. + (Note this new default only applies to new Rails apps that will be generated with + `config.active_record.belongs_to_required_by_default = true` in initializer.) + + *Josef Šimánek* + +* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type + columns. + + Fixes #17139. + + *Miklos Fazekas* + +* Format the time string according to the precision of the time column. + + *Ryuta Kamizono* + +* Allow a `:precision` option for time type columns. + + *Ryuta Kamizono* + +* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved + during the given block. + + For example, here's a pattern of creating notifications when new comments + are posted. (The notification may in turn trigger an email, a push + notification, or just appear in the UI somewhere): + + class Comment < ActiveRecord::Base + belongs_to :commentable, polymorphic: true + after_create -> { Notification.create! comment: self, + recipients: commentable.recipients } + end + + That's what you want the bulk of the time. A new comment creates a new + Notification. There may be edge cases where you don't want that, like + when copying a commentable and its comments, in which case write a + concern with something like this: + + module Copyable + def copy_to(destination) + Notification.suppress do + # Copy logic that creates new comments that we do not want triggering + # notifications. + end + end + end + + *Michael Ryan* + +* `:time` option added for `#touch`. + + Fixes #18905. + + *Hyonjee Joo* + +* Deprecate passing of `start` value to `find_in_batches` and `find_each` + in favour of `begin_at` value. + + *Vipul A M* + +* Add `foreign_key_exists?` method. + + *Tõnis Simo* + +* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods + if no block or limit is given, instead of loading the entire + collection into memory. This applies to relations (e.g. `User.all`) + as well as associations (e.g. `account.users`) + + # Before: + + users.none? + # SELECT "users".* FROM "users" + + users.one? + # SELECT "users".* FROM "users" + + # After: + + users.none? + # SELECT 1 AS one FROM "users" LIMIT 1 + + users.one? + # SELECT COUNT(*) FROM "users" + + *Eugene Gilburg* + +* Have `enum` perform type casting consistently with the rest of Active + Record, such as `where`. + + *Sean Griffin* + +* `scoping` no longer pollutes the current scope of sibling classes when using + STI. e.x. + + StiOne.none.scoping do + StiTwo.all + end + + Fixes #18806. + + *Sean Griffin* + +* `remove_reference` with `foreign_key: true` removes the foreign key before + removing the column. This fixes a bug where it was not possible to remove + the column on MySQL. + + Fixes #18664. + + *Yves Senn* + +* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start` + parameter to specify where to stop batch processing. + + *Vipul A M* + +* Fix a rounding problem for PostgreSQL timestamp columns. + + If a timestamp column has a precision specified, it needs to + format according to that. + + *Ryuta Kamizono* + +* Respect the database default charset for `schema_migrations` table. + + The charset of `version` column in `schema_migrations` table depends + on the database default charset and collation rather than the encoding + of the connection. + + *Ryuta Kamizono* + +* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. + + These are not valid values to merge in a relation, so it should warn users + early. + + *Rafael Mendonça França* + +* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file. + + This makes the db:structure tasks consistent with test:load_structure. + + *Dieter Komendera* + +* Respect custom primary keys for associations when calling `Relation#where` + + Fixes #18813. + + *Sean Griffin* + +* Fix several edge cases which could result in a counter cache updating + twice or not updating at all for `has_many` and `has_many :through`. + + Fixes #10865. + + *Sean Griffin* + +* Foreign keys added by migrations were given random, generated names. This + meant a different `structure.sql` would be generated every time a developer + ran migrations on their machine. + + The generated part of foreign key names is now a hash of the table name and + column name, which is consistent every time you run the migration. + + *Chris Sinjakli* + +* Validation errors would be raised for parent records when an association + was saved when the parent had `validate: false`. It should not be the + responsibility of the model to validate an associated object unless the + object was created or modified by the parent. + + This fixes the issue by skipping validations if the parent record is + persisted, not changed, and not marked for destruction. + + Fixes #17621. + + *Eileen M. Uchitelle, Aaron Patterson* + +* Fix n+1 query problem when eager loading nil associations (fixes #18312) + + *Sammy Larbi* + +* Change the default error message from `can't be blank` to `must exist` for + the presence validator of the `:required` option on `belongs_to`/`has_one` + associations. + + *Henrik Nygren* + +* Fixed `ActiveRecord::Relation#group` method when an argument is an SQL + reserved keyword: + + Example: + + SplitTest.group(:key).count + Property.group(:value).count + + *Bogdan Gusiev* + +* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR + operator to combine WHERE or HAVING clauses. + + Example: + + Post.where('id = 1').or(Post.where('id = 2')) + # => SELECT * FROM posts WHERE (id = 1) OR (id = 2) + + *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki* + +* Don't define autosave association callbacks twice from + `accepts_nested_attributes_for`. + + Fixes #18704. + + *Sean Griffin* + +* Integer types will no longer raise a `RangeError` when assigning an + attribute, but will instead raise when going to the database. + + Fixes several vague issues which were never reported directly. See the + commit message from the commit which added this line for some examples. + + *Sean Griffin* + +* Values which would error while being sent to the database (such as an + ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on + assignment. They will still error when sent to the database, but you are + given the ability to re-assign it to a valid value. + + Fixes #18580. + + *Sean Griffin* + +* Don't remove join dependencies in `Relation#exists?` + + Fixes #18632. + + *Sean Griffin* + +* Invalid values assigned to a JSON column are assumed to be `nil`. + + Fixes #18629. + + *Sean Griffin* + +* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly + discover which fields were read from a model when you are looking to only + select the data you need from the database. + + *Sean Griffin* + +* Introduce the `:if_exists` option for `drop_table`. + + Example: + + drop_table(:posts, if_exists: true) + + That would execute: + + DROP TABLE IF EXISTS posts + + If the table doesn't exist, `if_exists: false` (the default) raises an + exception whereas `if_exists: true` does nothing. + + *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono* + +* Don't run SQL if attribute value is not changed for update_attribute method. + + *Prathamesh Sonpatki* + +* `time` columns can now get affected by `time_zone_aware_attributes`. If you have + set `config.time_zone` to a value other than `'UTC'`, they will be treated + as in that time zone by default in Rails 5.1. If this is not the desired + behavior, you can set + + ActiveRecord::Base.time_zone_aware_types = [:datetime] + + A deprecation warning will be emitted if you have a `:time` column, and have + not explicitly opted out. + + Fixes #3145. + + *Sean Griffin* + +* Tests now run after_commit callbacks. You no longer have to declare + `uses_transaction ‘test name’` to test the results of an after_commit. + + after_commit callbacks run after committing a transaction whose parent + is not `joinable?`: un-nested transactions, transactions within test cases, + and transactions in `console --sandbox`. + + *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper* + +* `nil` as a value for a binary column in a query no longer logs as + "<NULL binary data>", and instead logs as just "nil". + + *Sean Griffin* + +* `attribute_will_change!` will no longer cause non-persistable attributes to + be sent to the database. + + Fixes #18407. + + *Sean Griffin* + +* Remove support for the `protected_attributes` gem. + + *Carlos Antonio da Silva*, *Roberto Miranda* + +* Fix accessing of fixtures having non-string labels like Fixnum. + + *Prathamesh Sonpatki* + * Remove deprecated support to preload instance-dependent associations. *Yves Senn* @@ -6,7 +634,7 @@ *Yves Senn* -* Remove deprecation when modifying a relation with cached arel. +* Remove deprecation when modifying a relation with cached Arel. This raises an `ImmutableRelation` error instead. *Yves Senn* @@ -29,12 +657,12 @@ *Rafael Mendonça França* -* Change transaction callbacks to not swallowing errors. +* Change transaction callbacks to not swallow errors. - Before this change any error raised inside a transaction callback are - rescued and printed in the logs. + Before this change any errors raised inside a transaction callback + were getting rescued and printed in the logs. - Now these errors are not rescue anymore and just bubble up, as the other callbacks. + Now these errors are not rescued anymore and just bubble up, as the other callbacks. *Rafael Mendonça França* @@ -54,7 +682,7 @@ *Rafael Mendonça França* -* Remove deprecated access to connection specification using a string acessor. +* Remove deprecated access to connection specification using a string accessor. Now all strings will be handled as a URL. @@ -84,7 +712,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 @@ -96,13 +724,13 @@ *Florian Weingarten* -* Fixed setting of foreign_key for through associations while building of new record. +* Fix setting of foreign_key for through associations when building a new record. Fixes #12698. *Ivan Antropov* -* Improve a dump of the primary key support. If it is not a default primary key, +* Improve dumping of the primary key. If it is not a default primary key, correctly dump the type and options. Fixes #14169, #16599. @@ -120,18 +748,18 @@ *Ryuta Kamizono* -* Allow precision option for MySQL datetimes. +* Allow a precision option for MySQL datetimes. *Ryuta Kamizono* -* Fixed automatic inverse_of for models nested in module. +* Fixed automatic `inverse_of` for models nested in a module. *Andrew McCloud* * Change `ActiveRecord::Relation#update` behavior so that it can be called without passing ids of the records to be updated. - This change allows to update multiple records returned by + This change allows updating multiple records returned by `ActiveRecord::Relation` with callbacks and validations. # Before @@ -163,6 +791,13 @@ *Rafael Mendonça França* +* Fix change detection problem for PostgreSQL bytea type and + `ArgumentError: string contains null byte` exception with pg-0.18. + + Fixes #17680. + + *Lars Kanis* + * When a table has a composite primary key, the `primary_key` method for SQLite3 and PostgreSQL adapters was only returning the first field of the key. Ensures that it will return nil instead, as Active Record doesn't support @@ -172,7 +807,7 @@ *arthurnn* -* `validates_size_of` / `validates_length_of` do not count records, +* `validates_size_of` / `validates_length_of` do not count records which are `marked_for_destruction?`. Fixes #7247. @@ -214,7 +849,7 @@ *Ryuta Kamizono* -* Support for any type primary key. +* Support for any type of primary key. Fixes #14194. @@ -240,7 +875,7 @@ *Yves Senn* -* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to +* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to be marked as having changed when set to the same negative value. Closes #18161. @@ -270,7 +905,7 @@ This option enables to define the column name of associated object's type for polymorphic associations. - *Ulisses Almeida, Kassio Borges* + *Ulisses Almeida*, *Kassio Borges* * Remove deprecated behavior allowing nested arrays to be passed as query values. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index f4777919d3..049c5d2b3b 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -31,8 +31,8 @@ which might look like this: PRIMARY KEY (id) ); -This would also define the following accessors: `Product#name` and -`Product#name=(new_name)`. +This would also define the following accessors: <tt>Product#name</tt> and +<tt>Product#name=(new_name)</tt>. * Associations between objects defined by simple class methods. @@ -188,7 +188,7 @@ Admit the Database: The latest version of Active Record can be installed with RubyGems: - % [sudo] gem install activerecord + % gem install activerecord Source code can be downloaded as part of the Rails project on GitHub: diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 7e3460365b..bae40604b1 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -16,7 +16,7 @@ To run a set of tests: You can also run tests that depend upon a specific database backend. For example: - $ bundle exec rake test_sqlite3 + $ bundle exec rake test:sqlite3 Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: @@ -24,6 +24,10 @@ Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: $ bundle exec rake test:mysql2 $ bundle exec rake test:postgresql $ bundle exec rake test:sqlite3 + +Using the SQLite3 adapter with an in-memory database is the fastest way +to run the tests: + $ bundle exec rake test:sqlite3_mem There should be tests available for each database backend listed in the {Config diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 976b559da9..a619204e6f 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' require File.expand_path(File.dirname(__FILE__)) + "/test/config" require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" @@ -51,7 +50,7 @@ end t.libs << 'test' t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { |x| x =~ /\/adapters\// - } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort + } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")) t.warning = true t.verbose = true @@ -122,7 +121,7 @@ namespace :db do # prepare hstore if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html" end end @@ -151,18 +150,3 @@ task :lines do files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end - -spec = eval(File.read('activerecord.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -# Publishing ------------------------------------------------------ - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index c5cd0c89f7..bd95b57303 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' 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 d9d47c3d99..1844b29ccb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -43,11 +43,13 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration + autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, 'active_record/migration' autoload :ModelSchema autoload :NestedAttributes autoload :NoTouching + autoload :TouchLater autoload :Persistence autoload :QueryCache autoload :Querying @@ -62,6 +64,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :Suppressor autoload :TableMetadata autoload :Timestamp autoload :Transactions diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 1040e6e3bb..f7b50cd25a 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -3,10 +3,27 @@ module ActiveRecord module Aggregations # :nodoc: extend ActiveSupport::Concern - def clear_aggregation_cache #:nodoc: - @aggregation_cache.clear if persisted? + def initialize_dup(*) # :nodoc: + @aggregation_cache = {} + super end + def reload(*) # :nodoc: + clear_aggregation_cache + super + end + + private + + def clear_aggregation_cache # :nodoc: + @aggregation_cache.clear if persisted? + end + + def init_internals # :nodoc: + @aggregation_cache = {} + super + end + # Active Record implements aggregation through a macro-like class method called +composed_of+ # for representing attributes as value objects. It expresses relationships like "Account [is] # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call @@ -87,11 +104,6 @@ module ActiveRecord # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") # - # customer.address_street = "Vesterbrogade" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # customer.clear_aggregation_cache - # customer.address # => Address.new("Vesterbrogade", "Copenhagen") - # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" @@ -130,7 +142,7 @@ module ActiveRecord # converted to an instance of value class if necessary. # # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be - # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). + # aggregated using the NetAddr::CIDR value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. # New values can be assigned to the value object using either another NetAddr::CIDR object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet @@ -245,7 +257,8 @@ module ActiveRecord define_method("#{name}=") do |part| klass = class_name.constantize if part.is_a?(Hash) - part = klass.new(*part.values) + raise ArgumentError unless part.size == part.keys.max + part = klass.new(*part.sort.map(&:last)) end unless part.is_a?(klass) || converter.nil? || part.nil? diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index f2b44913db..ee0bb8fafe 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -13,6 +13,19 @@ module ActiveRecord other == to_a end + def build(*args, &block) + scoping { @association.build(*args, &block) } + end + alias new build + + def create(*args, &block) + scoping { @association.create(*args, &block) } + end + + def create!(*args, &block) + scoping { @association.create!(*args, &block) } + end + private def exec_queries diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 14af55f327..1ca648d48d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -113,19 +113,19 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. - autoload :Association, 'active_record/associations/association' - autoload :SingularAssociation, 'active_record/associations/singular_association' - autoload :CollectionAssociation, 'active_record/associations/collection_association' - autoload :ForeignAssociation, 'active_record/associations/foreign_association' - autoload :CollectionProxy, 'active_record/associations/collection_proxy' + autoload :Association + autoload :SingularAssociation + autoload :CollectionAssociation + autoload :ForeignAssociation + autoload :CollectionProxy - autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' - autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasManyAssociation, 'active_record/associations/has_many_association' - autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' - autoload :HasOneAssociation, 'active_record/associations/has_one_association' - autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' - autoload :ThroughAssociation, 'active_record/associations/through_association' + autoload :BelongsToAssociation + autoload :BelongsToPolymorphicAssociation + autoload :HasManyAssociation + autoload :HasManyThroughAssociation + autoload :HasOneAssociation + autoload :HasOneThroughAssociation + autoload :ThroughAssociation module Builder #:nodoc: autoload :Association, 'active_record/associations/builder/association' @@ -139,26 +139,20 @@ module ActiveRecord end eager_autoload do - autoload :Preloader, 'active_record/associations/preloader' - autoload :JoinDependency, 'active_record/associations/join_dependency' - autoload :AssociationScope, 'active_record/associations/association_scope' - autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :Preloader + autoload :JoinDependency + autoload :AssociationScope + autoload :AliasTracker end - # Clears out the association cache. - def clear_association_cache #:nodoc: - @association_cache.clear if persisted? - end - - # :nodoc: - attr_reader :association_cache - # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) if association.nil? - raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) + unless reflection = self.class._reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) + end association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -166,8 +160,32 @@ module ActiveRecord association end + def association_cached?(name) # :nodoc + @association_cache.key?(name) + end + + def initialize_dup(*) # :nodoc: + @association_cache = {} + super + end + + def reload(*) # :nodoc: + clear_association_cache + super + end + private - # Returns the specified association instance if it responds to :loaded?, nil otherwise. + # Clears out the association cache. + def clear_association_cache # :nodoc: + @association_cache.clear if persisted? + end + + def init_internals # :nodoc: + @association_cache = {} + super + end + + # Returns the specified association instance if it exists, nil otherwise. def association_instance_get(name) @association_cache[name] end @@ -248,7 +266,6 @@ module ActiveRecord # others.find(*args) | X | X | X # others.exists? | X | X | X # others.distinct | X | X | X - # others.uniq | X | X | X # others.reset | X | X | X # # === Overriding generated methods @@ -267,7 +284,7 @@ module ActiveRecord # end # # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods module is # included in the model class immediately after the (anonymous) generated attributes methods # module, meaning an association will override the methods for an attribute with the same name. # @@ -1000,6 +1017,8 @@ module ActiveRecord # callbacks declared either before or after the <tt>:dependent</tt> option # can affect what it does. # + # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. + # # === Delete or destroy? # # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, @@ -1012,7 +1031,7 @@ module ActiveRecord # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # @@ -1245,6 +1264,10 @@ module ActiveRecord # that is the inverse of this <tt>has_many</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:extend] + # Specifies a module or array of modules that will be extended into the association object returned. + # Useful for defining methods on associations, especially when they should be shared between multiple + # association objects. # # Option examples: # has_many :comments, -> { order "posted_on" } @@ -1326,6 +1349,8 @@ module ActiveRecord # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object + # + # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association @@ -1378,7 +1403,7 @@ module ActiveRecord # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment" # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" # has_one :attachment, as: :attachable - # has_one :boss, readonly: :true + # has_one :boss, -> { readonly } # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable # has_one :credit_card, required: true @@ -1430,7 +1455,7 @@ module ActiveRecord # when you access the associated object. # # Scope examples: - # belongs_to :user, -> { where(id: 2) } + # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } # belongs_to :level, ->(level) { where("game_level > ?", level.current) } # @@ -1494,10 +1519,14 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:optional] + # When set to +true+, the association will not have its presence validated. # [:required] # When set to +true+, the association will also have its presence validated. # This will validate the association itself, not the id. You can use # +:inverse_of+ to avoid an extra query during validation. + # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If + # you don't want to have association presence validated, use <tt>optional: true</tt>. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1506,11 +1535,11 @@ module ActiveRecord # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true - # belongs_to :project, readonly: true + # belongs_to :project, -> { readonly } # belongs_to :post, counter_cache: true - # belongs_to :company, touch: true + # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at - # belongs_to :company, required: true + # belongs_to :user, optional: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1535,10 +1564,7 @@ module ActiveRecord # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration # def change - # create_table :developers_projects, id: false do |t| - # t.integer :developer_id - # t.integer :project_id - # end + # create_join_table :developers, :projects # end # end # @@ -1688,16 +1714,14 @@ module ActiveRecord join_model = builder.through_model - # FIXME: we should move this to the internal constants. Also people - # should never directly access this constant so I'm not happy about - # setting it. const_set join_model.name, join_model + private_constant join_model.name middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection - middle_reflection.parent_reflection = [name.to_s, habtm_reflection] + middle_reflection.parent_reflection = habtm_reflection include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1713,12 +1737,12 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k| hm_options[k] = options[k] if options.key? k end has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] + self._reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 0d8e4ba870..930f678ae8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -8,12 +8,12 @@ module ActiveRecord # # Association # SingularAssociation - # HasOneAssociation + # HasOneAssociation + ForeignAssociation # HasOneThroughAssociation + ThroughAssociation # BelongsToAssociation # BelongsToPolymorphicAssociation # CollectionAssociation - # HasManyAssociation + # HasManyAssociation + ForeignAssociation # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index d06b7b3508..2416167834 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -2,42 +2,30 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: def self.scope(association, connection) - INSTANCE.scope association, connection - end - - class BindSubstitution - def initialize(block) - @block = block - end - - def bind_value(scope, column, value, connection) - substitute = connection.substitute_at(column) - scope.bind_values += [[column, @block.call(value)]] - substitute - end + INSTANCE.scope(association, connection) end def self.create(&block) - block = block ? block : lambda { |val| val } - new BindSubstitution.new(block) + block ||= lambda { |val| val } + new(block) end - def initialize(bind_substitution) - @bind_substitution = bind_substitution + def initialize(value_transformation) + @value_transformation = value_transformation end INSTANCE = create def scope(association, connection) - klass = association.klass - reflection = association.reflection - scope = klass.unscoped - owner = association.owner + klass = association.klass + reflection = association.reflection + scope = klass.unscoped + owner = association.owner alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster chain_head, chain_tail = get_chain(reflection, association, alias_tracker) scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, connection, chain_head, chain_tail) + add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) end def join_type @@ -61,43 +49,36 @@ module ActiveRecord binds end + protected + + attr_reader :value_transformation + private def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - def column_for(table_name, column_name, connection) - columns = connection.schema_cache.columns_hash(table_name) - columns[column_name] - end - - def bind_value(scope, column, value, connection) - @bind_substitution.bind_value scope, column, value, connection - end - - def bind(scope, table_name, column_name, value, connection) - column = column_for table_name, column_name, connection - bind_value scope, column, value, connection - end - - def last_chain_scope(scope, table, reflection, owner, connection, association_klass) + def last_chain_scope(scope, table, reflection, owner, association_klass) join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key - bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], connection - scope = scope.where(table[key].eq(bind_val)) + value = transform_value(owner[foreign_key]) + scope = scope.where(table.name => { key => value }) if reflection.type - value = owner.class.base_class.name - bind_val = bind scope, table.table_name, reflection.type, value, connection - scope = scope.where(table[reflection.type].eq(bind_val)) - else - scope + polymorphic_type = transform_value(owner.class.base_class.name) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) end + + scope + end + + def transform_value(value) + value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) + def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key @@ -105,9 +86,8 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = next_reflection.klass.base_class.name - bind_val = bind scope, table.table_name, reflection.type, value, connection - scope = scope.where(table[reflection.type].eq(bind_val)) + value = transform_value(next_reflection.klass.base_class.name) + scope = scope.where(table.name => { reflection.type => value }) end scope = scope.joins(join(foreign_table, constraint)) @@ -138,10 +118,10 @@ module ActiveRecord [runtime_reflection, previous_reflection] end - def add_constraints(scope, owner, association_klass, refl, connection, chain_head, chain_tail) + def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) owner_reflection = chain_tail table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, connection, association_klass) + scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) reflection = chain_head loop do @@ -151,7 +131,7 @@ module ActiveRecord unless reflection == chain_tail next_reflection = reflection.next foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) end # Exclude the scope of the association itself, because that @@ -160,15 +140,14 @@ module ActiveRecord item = eval_scope(reflection.klass, scope_chain_item, owner) if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes, :bind) + scope.merge! item.except(:where, :includes) end reflection.all_includes do scope.includes! item.includes_values end - scope.where_values += item.where_values - scope.bind_values += item.bind_values + scope.where_clause += item.where_clause scope.order_values |= item.order_values end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c63b42e2a0..265a65c4c1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -68,6 +68,9 @@ module ActiveRecord def increment_counter(counter_cache_name) if foreign_key_present? klass.increment_counter(counter_cache_name, target_id) + if target && !stale_target? + target.increment(counter_cache_name) + end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 88406740d8..ba1b1814d1 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: + VALID_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index d0ad57f9c6..97eb007f62 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def self.valid_options(options) - super + [:foreign_type, :polymorphic, :touch, :counter_cache] + super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -60,7 +60,7 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch) # :nodoc: + def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id @@ -75,9 +75,9 @@ module ActiveRecord::Associations::Builder if old_record if touch != true - old_record.touch touch + old_record.send(touch_method, touch) else - old_record.touch + old_record.send(touch_method) end end end @@ -85,9 +85,9 @@ module ActiveRecord::Associations::Builder record = o.send name if record && record.persisted? if touch != true - record.touch touch + record.send(touch_method, touch) else - record.touch + record.send(touch_method) end end end @@ -98,7 +98,8 @@ module ActiveRecord::Associations::Builder touch = reflection.options[:touch] callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch) + touch_method = touching_delayed_records? ? :touch : :touch_later + BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) } model.after_save callback, if: :changed? @@ -110,5 +111,23 @@ module ActiveRecord::Associations::Builder name = reflection.name model.after_destroy lambda { |o| o.association(name).handle_dependency } end + + def self.define_validations(model, reflection) + if reflection.options.key?(:required) + reflection.options[:optional] = !reflection.options.delete(:required) + end + + if reflection.options[:optional].nil? + required = model.belongs_to_required_by_default + else + required = !reflection.options[:optional] + end + + super + + if required + model.validates_presence_of reflection.name, message: :required + end + 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 93dc4ae118..ffd9c9d6fc 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 @@ -78,14 +78,14 @@ module ActiveRecord::Associations::Builder join_model.table_name_resolver = habtm join_model.class_resolver = lhs_model - join_model.add_left_association :left_side, class: lhs_model + join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) join_model end def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_').gsub(/::/, '_').to_sym + association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 64e9e6b334..a272d3c781 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -17,5 +17,12 @@ module ActiveRecord::Associations::Builder def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name, message: :required + end + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 1369212837..42542f188e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -27,12 +27,5 @@ module ActiveRecord::Associations::Builder end CODE end - - def self.define_validations(model, reflection) - super - if reflection.options[:required] - model.validates_presence_of reflection.name - end - end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f2c96e9a2a..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,10 +33,10 @@ module ActiveRecord reload end - if owner.new_record? + if null_scope? # Cache the proxy separately before the owner has an id # or else a post-save proxy will still lack the id - @new_record_proxy ||= CollectionProxy.create(klass, self) + @null_proxy ||= CollectionProxy.create(klass, self) else @proxy ||= CollectionProxy.create(klass, self) end @@ -63,7 +63,7 @@ module ActiveRecord def ids_writer(ids) pk_type = reflection.primary_key_type ids = Array(ids).reject(&:blank?) - ids.map! { |i| pk_type.type_cast_from_user(i) } + ids.map! { |i| pk_type.cast(i) } replace(klass.find(ids).index_by(&:id).values_at(*ids)) end @@ -129,6 +129,16 @@ module ActiveRecord first_nth_or_last(:last, *args) end + def take(n = nil) + if loaded? + n ? target.take(n) : target.first + else + scope.take(n).tap do |record| + set_inverse_instance record if record.is_a? ActiveRecord::Base + end + end + end + def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } @@ -151,6 +161,7 @@ module ActiveRecord # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. def concat(*records) + records = records.flatten if owner.new_record? load_target concat_records(records) @@ -318,7 +329,8 @@ module ActiveRecord end # Returns true if the collections is not empty. - # Equivalent to +!collection.empty?+. + # If block given, loads all records and checks for one or more matches. + # Otherwise, equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -328,7 +340,8 @@ module ActiveRecord end # Returns true if the collection has more than 1 record. - # Equivalent to +collection.size > 1+. + # If block given, loads all records and checks for two or more matches. + # Otherwise, equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -357,6 +370,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end @@ -419,8 +434,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.to_a end @@ -549,7 +563,7 @@ module ActiveRecord def concat_records(records, should_raise = false) result = true - records.flatten.each do |record| + records.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do |rec| result &&= insert_record(rec, true, should_raise) unless owner.new_record? diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index c22dc6e11e..ddeafb40ea 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -227,6 +227,10 @@ module ActiveRecord @association.last(*args) end + def take(n = nil) + @association.take(n) + end + # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array @@ -466,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 @@ -972,6 +977,9 @@ module ActiveRecord # Equivalent to +delete_all+. The difference is that returns +self+, instead # of an array with the deleted objects, so methods can be chained. See # +delete_all+ for more information. + # Note that because +delete_all+ removes records by directly + # running an SQL query into the database, the +updated_at+ column of + # the object is not changed. def clear delete_all self diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2a782c06d0..ca27c9fdde 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -85,7 +85,11 @@ module ActiveRecord end def cached_counter_attribute_name(reflection = reflection()) - options[:counter_cache] || "#{reflection.name}_count" + if reflection.options[:counter_cache] + reflection.options[:counter_cache].to_s + else + "#{reflection.name}_count" + end end def update_counter(difference, reflection = reflection()) @@ -101,7 +105,7 @@ module ActiveRecord end def update_counter_in_memory(difference, reflection = reflection()) - if has_cached_counter?(reflection) + if counter_must_be_updated_by_has_many?(reflection) counter = cached_counter_attribute_name(reflection) owner[counter] += difference owner.send(:clear_attribute_changes, counter) # eww @@ -118,18 +122,28 @@ module ActiveRecord # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = reflection()) + def inverse_which_updates_counter_cache(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - inverse_updates_counter_named?(counter_name, reflection) + inverse_which_updates_counter_named(counter_name, reflection) end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - def inverse_updates_counter_named?(counter_name, reflection = reflection()) - reflection.klass._reflections.values.any? { |inverse_reflection| + def inverse_which_updates_counter_named(counter_name, reflection) + reflection.klass._reflections.values.find { |inverse_reflection| inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end + def inverse_updates_counter_in_memory?(reflection) + inverse = inverse_which_updates_counter_cache(reflection) + inverse && inverse == reflection.inverse_of + end + + def counter_must_be_updated_by_has_many?(reflection) + !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) + end + def delete_count(method, scope) if method == :delete_all scope.delete_all 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 f1e784d771..cd79266952 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,12 +38,10 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) end save_through_record(record) @@ -135,7 +133,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each(&:_run_destroy_callbacks) + scope.each { |record| record.run_callbacks :destroy } arel = scope.arel @@ -143,7 +141,7 @@ module ActiveRecord stmt.from scope.klass.arel_table stmt.wheres = arel.constraints - count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values) + count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes) end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) @@ -160,9 +158,9 @@ module ActiveRecord if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) + else + update_counter(-count) end - - update_counter(-count) end def through_records_for(record) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 4b75370171..81eb5136a1 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -232,23 +232,26 @@ module ActiveRecord end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? primary_id = ar_parent.id parent.children.each do |node| if node.reflection.collection? other = ar_parent.association(node.reflection.name) other.loaded! - else - if ar_parent.association_cache.key?(node.reflection.name) - model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) - next - end + elsif ar_parent.association_cached?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next end key = aliases.column_alias(node, node.primary_key) id = row[key] - next if id.nil? + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end model = seen[parent.base_klass][primary_id][node.base_klass][id] diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index c1ef86a95b..a6ad09a38a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -25,7 +25,7 @@ module ActiveRecord def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] - bind_values = [] + binds = [] tables = tables.reverse scope_chain_index = 0 @@ -66,7 +66,7 @@ module ActiveRecord end if rel && !rel.arel.constraints.empty? - bind_values.concat rel.bind_values + binds += rel.bound_attributes constraint = constraint.and rel.arel.constraints end @@ -75,7 +75,7 @@ module ActiveRecord column = klass.columns_hash[reflection.type.to_s] substitute = klass.connection.substitute_at(column) - bind_values.push [column, value] + binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) constraint = constraint.and table[reflection.type].eq substitute end @@ -85,7 +85,7 @@ module ActiveRecord foreign_table, foreign_klass = table, klass end - JoinInformation.new joins, bind_values + JoinInformation.new joins, binds end # Builds equality condition. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 4358f3b581..97f4bd3811 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -89,7 +89,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index afcaa5d55a..1dc8bff193 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_for_attribute(association_key_name).type + @klass.type_for_attribute(association_key_name.to_s).type end def owner_key_type - @model.column_for_attribute(owner_key_name).type + @model.type_for_attribute(owner_key_name.to_s).type end def load_slices(slices) @@ -131,18 +131,19 @@ module ActiveRecord def build_scope scope = klass.unscoped - values = reflection_scope.values - reflection_binds = reflection_scope.bind_values + values = reflection_scope.values preload_values = preload_scope.values - preload_binds = preload_scope.bind_values - scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope.bind_values = (reflection_binds + preload_binds) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] - scope.joins! preload_values[:joins] || values[:joins] + if preload_scope.joins_values.any? + scope.joins!(preload_scope.joins_values) + else + scope.joins!(reflection_scope.joins_values) + end scope.order! preload_values[:order] || values[:order] if preload_values[:readonly] || values[:readonly] diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 12bf3ef138..56aa23b173 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -78,10 +78,9 @@ module ActiveRecord if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] else - unless reflection_scope.where_values.empty? + unless reflection_scope.where_clause.empty? scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - scope.where_values = reflection_scope.values[:where] - scope.bind_values = reflection_scope.bind_values + scope.where_clause = reflection_scope.where_clause end scope.references! reflection_scope.values[:references] diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c44242a0f0..58d0f7d65d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -41,8 +41,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.limit(1).to_a end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 09828dbd9b..af1bce523c 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -18,7 +18,7 @@ module ActiveRecord reflection_scope = reflection.scope if reflection_scope && reflection_scope.arity.zero? - relation.merge!(reflection_scope) + relation = relation.merge(reflection_scope) end scope.merge!( @@ -33,7 +33,7 @@ module ActiveRecord # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # - # We only support indirectly modifying through associations which has a belongs_to source. + # We only support indirectly modifying through associations which have a belongs_to source. # This is the "has_many :tags, through: :taggings" situation, where the join model # typically has a belongs_to on both side. In other words, associations which could also # be represented as has_and_belongs_to_many associations. diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 88536eaac0..73dd3fa041 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -43,7 +43,7 @@ module ActiveRecord end def value_for_database - type.type_cast_for_database(value) + type.serialize(value) end def changed_from?(old_value) @@ -51,7 +51,7 @@ module ActiveRecord end def changed_in_place_from?(old_value) - type.changed_in_place?(old_value, value) + has_been_read? && type.changed_in_place?(old_value, value) end def with_value_from_user(value) @@ -66,6 +66,10 @@ module ActiveRecord self.class.with_cast_value(name, value, type) end + def with_type(type) + self.class.new(name, value_before_type_cast, type) + end + def type_cast(*) raise NotImplementedError end @@ -74,12 +78,25 @@ module ActiveRecord true end + def came_from_user? + false + end + + def has_been_read? + defined?(@value) + end + def ==(other) self.class == other.class && name == other.name && value_before_type_cast == other.value_before_type_cast && type == other.type end + alias eql? == + + def hash + [self.class, name, value_before_type_cast, type].hash + end protected @@ -91,13 +108,17 @@ module ActiveRecord class FromDatabase < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_database(value) + type.deserialize(value) end end class FromUser < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_user(value) + type.cast(value) + end + + def came_from_user? + true end end @@ -120,6 +141,10 @@ module ActiveRecord nil end + def with_type(type) + self.class.with_cast_value(name, nil, type) + end + def with_value_from_database(value) raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" end @@ -144,6 +169,6 @@ module ActiveRecord false end end - private_constant :FromDatabase, :FromUser, :Null, :Uninitialized + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb new file mode 100644 index 0000000000..e0bee8c17e --- /dev/null +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -0,0 +1,32 @@ +require 'active_record/attribute' + +module ActiveRecord + class Attribute # :nodoc: + class UserProvidedDefault < FromUser + def initialize(name, value, type, database_default) + super(name, value, type) + @database_default = database_default + end + + def type_cast(value) + if value.is_a?(Proc) + super(value.call) + else + super + end + end + + def changed_in_place_from?(old_value) + super || changed_from?(database_default.value) + end + + def with_type(type) + self.class.new(name, value_before_type_cast, type, database_default) + end + + protected + + attr_reader :database_default + end + end +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf64830417..45fdcaa1cd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,61 +3,38 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::ForbiddenAttributesProtection - - # Allows you to set all the attributes by passing in a hash of attributes with - # keys matching the attribute names (which again matches the column names). - # - # If the passed hash responds to <tt>permitted?</tt> method and the return value - # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> - # exception is raised. - # - # cat = Cat.new(name: "Gorby", status: "yawning") - # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} - # cat.assign_attributes(status: "sleeping") - # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } - # - # New attributes will be persisted in the database when the object is saved. - # - # Aliased to <tt>attributes=</tt>. - def assign_attributes(new_attributes) - if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." - end - return if new_attributes.blank? + include ActiveModel::AttributeAssignment + + # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + def attributes=(attributes) + assign_attributes(attributes) + end - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - nested_parameter_attributes = [] + private - attributes = sanitize_for_mass_assignment(attributes) + def _assign_attributes(attributes) # :nodoc: + multi_parameter_attributes = {} + nested_parameter_attributes = {} attributes.each do |k, v| if k.include?("(") - multi_parameter_attributes << [ k, v ] + multi_parameter_attributes[k] = attributes.delete(k) elsif v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - _assign_attribute(k, v) + nested_parameter_attributes[k] = attributes.delete(k) end end + super(attributes) assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - alias attributes= assign_attributes - - private - - def _assign_attribute(k, v) - public_send("#{k}=", v) - rescue NoMethodError - if respond_to?("#{k}=") - raise - else - raise UnknownAttributeError.new(self, k) - end + # Tries to assign given value to given attribute. + # In case of an error, re-raises with the ActiveRecord constant. + def _assign_attribute(k, v) # :nodoc: + super + rescue ActiveModel::UnknownAttributeError + raise UnknownAttributeError.new(self, k) end # Assign any deferred nested attributes after the base attributes have been set. @@ -81,7 +58,12 @@ module ActiveRecord errors = [] callstack.each do |name, values_with_empty_parameters| begin - send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters + end + send("#{name}=", values) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end @@ -113,100 +95,5 @@ module ActiveRecord def find_parameter_position(multiparameter_name) multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end - - class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :cast_type - - def initialize(object, name, values) - @object = object - @name = name - @values = values - end - - def read_value - return if values.values.compact.empty? - - @cast_type = object.type_for_attribute(name) - klass = cast_type.klass - - if klass == Time - read_time - elsif klass == Date - read_date - else - read_other - end - end - - private - - def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) - Time.zone.local(*set_values) - else - Time.send(object.class.default_timezone, *set_values) - end - end - - def read_time - # If column is a :time (and not :date or :datetime) there is no need to validate if - # there are year/month/day fields - if cast_type.type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| - values[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - validate_required_parameters!([1,2,3]) - - # If Date bits were provided but blank, then return nil - return if blank_date_parameter? - end - - max_position = extract_max_param(6) - set_values = values.values_at(*(1..max_position)) - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } - instantiate_time_object(set_values) - end - - def read_date - return if blank_date_parameter? - set_values = values.values_at(1,2,3) - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other - max_position = extract_max_param - positions = (1..max_position) - validate_required_parameters!(positions) - - values.slice(*positions) - end - - # Checks whether some blank date parameter exists. Note that this is different - # than the validate_required_parameters! method, since it just checks for blank - # positions instead of missing ones, and does not raise in case one blank position - # exists. The caller is responsible to handle the case of this returning true. - def blank_date_parameter? - (1..3).any? { |position| values[position].blank? } - end - - # If some position is not provided, it errors out a missing parameter exception. - def validate_required_parameters!(positions) - if missing_parameter = positions.detect { |position| !values.key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") - end - end - - def extract_max_param(upper_cap = 100) - [values.keys.max, upper_cap].min - end - end end end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 5b96623b6e..7d0ae32411 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -15,7 +15,7 @@ module ActiveRecord end def decorate_matching_attribute_types(matcher, decorator_name, &block) - clear_caches_calculated_from_columns + reload_schema_from_cache decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes @@ -24,10 +24,11 @@ module ActiveRecord private - def add_user_provided_columns(*) - super.map do |column| - decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) - column.with_type(decorated_type) + def load_schema! + super + attribute_types.each do |name, type| + decorated_type = attribute_type_decorations.apply(name, type) + define_attribute(name, decorated_type) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 8f165fb1dc..9d58a19304 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -83,7 +83,7 @@ module ActiveRecord generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class - super(column_names) + super(attribute_names) @attribute_methods_generated = true end true @@ -185,7 +185,7 @@ module ActiveRecord # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? - column_names + attribute_types.keys else [] end @@ -369,6 +369,39 @@ module ActiveRecord write_attribute(attr_name, value) end + # Returns the name of all database fields which have been read from this + # model. This can be useful in development mode to determine which fields + # need to be selected. For performance critical pages, selecting only the + # required fields can be an easy performance win (assuming you aren't using + # all of the fields on the model). + # + # For example: + # + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index + # + # def index + # @posts = Post.all + # end + # + # private + # + # def print_accessed_fields + # p @posts.first.accessed_fields + # 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) + # end + # end + def accessed_fields + @attributes.accessed + end + protected def clone_attribute_value(reader_method, attribute_name) # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index fd61febd57..56c1898551 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -28,6 +28,7 @@ module ActiveRecord included do attribute_method_suffix "_before_type_cast" + attribute_method_suffix "_came_from_user?" end # Returns the value of the attribute identified by +attr_name+ before @@ -66,6 +67,10 @@ module ActiveRecord def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end + + def attribute_came_from_user?(attribute_name) + @attributes[attribute_name].came_from_user? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index d5702accaf..0171ef3bdf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -108,7 +108,8 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) - if attribute_changed?(attr) + clear_changed_attributes_cache + if attribute_changed_by_setter?(attr) clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) @@ -131,10 +132,8 @@ module ActiveRecord partial_writes? ? super(keys_for_partial_write) : super end - # Serialized attributes should always be written in case they've been - # changed in place. def keys_for_partial_write - changed + changed & self.class.column_names end def _field_changed?(attr, old_value) @@ -165,7 +164,7 @@ module ActiveRecord end def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil end def store_original_raw_attributes @@ -178,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/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index dc689f399a..553122a5fc 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -22,7 +22,7 @@ module ActiveRecord return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) !value.blank? end - elsif column.number? + elsif value.respond_to?(:zero?) !value.zero? else !value.blank? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4b72fe7d7e..0d989c2eca 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -69,15 +69,21 @@ module ActiveRecord # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API - def _read_attribute(attr_name) # :nodoc: - @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) + end + else + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + end end - private + alias :attribute :_read_attribute + private :attribute - def attribute(attribute_name) - _read_attribute(attribute_name) - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d0d8a968c5..fd7099e0de 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,6 +11,15 @@ module ActiveRecord # serialized object must be of that class on assignment and retrieval. # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # + # 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/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 777f7ab4d7..f9beb43e4b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,24 +2,26 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: - include Type::Decorator - - def type_cast_from_database(value) + def deserialize(value) convert_time_to_time_zone(super) end - def type_cast_from_user(value) + def cast(value) if value.is_a?(Array) - value.map { |v| type_cast_from_user(v) } + value.map { |v| cast(v) } + elsif value.is_a?(Hash) + set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - value.in_time_zone || super + user_input_in_time_zone(value) || super rescue ArgumentError nil end end end + private + def convert_time_to_time_zone(value) if value.is_a?(Array) value.map { |v| convert_time_to_time_zone(v) } @@ -29,6 +31,10 @@ module ActiveRecord value end end + + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).in_time_zone + end end extend ActiveSupport::Concern @@ -39,6 +45,9 @@ module ActiveRecord class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false self.skip_time_zone_conversion_for_attributes = [] + + class_attribute :time_zone_aware_types, instance_writer: false + self.time_zone_aware_types = [:datetime, :not_explicitly_configured] end module ClassMethods @@ -59,9 +68,31 @@ module ActiveRecord end def create_time_zone_conversion_attribute?(name, cast_type) - time_zone_aware_attributes && - !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == cast_type.type) + enabled_for_column = time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + result = enabled_for_column && + time_zone_aware_types.include?(cast_type.type) + + if enabled_for_column && + !result && + cast_type.type == :time && + time_zone_aware_types.include?(:not_explicitly_configured) + ActiveSupport::Deprecation.warn(<<-MESSAGE) + Time columns will become time zone aware in Rails 5.1. This + still causes `String`s to be parsed as if they were in `Time.zone`, + and `Time`s to be converted to `Time.zone`. + + To keep the old behavior, you must add the following to your initializer: + + config.active_record.time_zone_aware_types = [:datetime] + + To silence this deprecation warning, add the following: + + config.active_record.time_zone_aware_types << :time + MESSAGE + end + + result end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 66fcaf6945..013a7d0e01 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -10,6 +10,10 @@ module ActiveRecord attributes[name] || Attribute.null(name) end + def []=(name, value) + attributes[name] = value + end + def values_before_type_cast attributes.transform_values(&:value_before_type_cast) end @@ -24,11 +28,19 @@ module ActiveRecord end def keys - attributes.initialized_keys + attributes.each_key.select { |name| self[name].initialized? } end - def fetch_value(name) - self[name].value { |n| yield n if block_given? } + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def fetch_value(name, &block) + self[name].value(&block) + end + else + def fetch_value(name) + self[name].value { |n| yield n if block_given? } + end end def write_from_database(name, value) @@ -49,7 +61,7 @@ module ActiveRecord end def initialize_dup(_) - @attributes = attributes.dup + @attributes = attributes.deep_dup super end @@ -64,6 +76,10 @@ module ActiveRecord end end + def accessed + attributes.select { |_, attr| attr.has_been_read? }.keys + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 3a76f5262d..e85777c335 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -20,7 +20,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :select, :transform_values, to: :materialize + delegate :transform_values, :each_key, to: :materialize def initialize(types, values, additional_types) @types = types @@ -45,15 +45,21 @@ module ActiveRecord delegate_hash[key] = value end - def initialized_keys - delegate_hash.keys | values.keys - end - def initialize_dup(_) @delegate_hash = delegate_hash.transform_values(&:dup) super end + def select + keys = types.keys | values.keys | delegate_hash.keys + keys.each_with_object({}) do |key, hash| + attribute = self[key] + if yield(key, attribute) + hash[key] = attribute + end + end + end + protected attr_reader :types, :values, :additional_types, :delegate_hash diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index aafb990bc1..8b2c4c7170 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,31 +1,47 @@ +require 'active_record/attribute/user_provided_default' + module ActiveRecord - module Attributes # :nodoc: + # See ActiveRecord::Attributes::ClassMethods for documentation + module Attributes extend ActiveSupport::Concern + # :nodoc: Type = ActiveRecord::Type included do - class_attribute :user_provided_columns, instance_accessor: false # :internal: - class_attribute :user_provided_defaults, instance_accessor: false # :internal: - self.user_provided_columns = {} - self.user_provided_defaults = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: + self.attributes_to_define_after_schema_loads = {} end - module ClassMethods # :nodoc: - # Defines or overrides a attribute on this model. This allows customization of - # Active Record's type casting behavior, as well as adding support for user defined - # types. + module ClassMethods + # Defines an attribute with a type on this model. It will override the + # type of existing attributes if needed. This allows control over how + # values are converted to and from SQL when assigned to a model. It also + # changes the behavior of values passed to + # ActiveRecord::QueryMethods#where. This will let you use + # your domain objects across much of Active Record, without having to + # rely on implementation details or monkey patching. # - # +name+ The name of the methods to define attribute methods for, and the column which - # this will persist to. + # +name+ The name of the methods to define attribute methods for, and the + # column which this will persist to. # - # +cast_type+ A type object that contains information about how to type cast the value. - # See the examples section for more information. + # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object + # to be used for this attribute. See the examples below for more + # information about providing custom type objects. # # ==== Options - # The options hash accepts the following options: # - # +default+ is the default value that the column should use on a new record. + # The following options are accepted: + # + # +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+. + # + # +array+ (PG only) specifies that the type should be an array (see the + # examples below). + # + # +range+ (PG only) specifies that the type should be a range (see the + # examples below). # # ==== Examples # @@ -46,94 +62,201 @@ module ActiveRecord # store_listing.price_in_cents # => BigDecimal.new(10.1) # # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, Type::Integer.new + # attribute :price_in_cents, :integer # end # # # after # store_listing.price_in_cents # => 10 # - # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The +type_cast+ method on your type object will be called - # with values both from the database, and from your controllers. See - # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your - # type objects inherit from an existing type, or the base value type. + # A default can also be provided. + # + # create_table :store_listings, force: true do |t| + # t.string :my_string, default: "original default" + # end + # + # StoreListing.new.my_string # => "original default" + # + # class StoreListing < ActiveRecord::Base + # attribute :my_string, :string, default: "new default" + # end + # + # 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 + # attribute :my_string, :string + # attribute :my_int_array, :integer, array: true + # attribute :my_float_range, :float, range: true + # end + # + # model = MyModel.new( + # my_string: "string", + # my_int_array: ["1", "2", "3"], + # my_float_range: "[1,3.5]", + # ) + # model.attributes + # # => + # { + # my_string: "string", + # my_int_array: [1, 2, 3], + # my_float_range: 1.0..3.5 + # } + # + # ==== Creating Custom Types + # + # Users may also define their own custom types, as long as they respond + # to the methods defined on the value type. The method +deserialize+ or + # +cast+ will be called on your type object, with raw input from the + # database or from your controllers. See ActiveRecord::Type::Value for the + # expected API. It is recommended that your type objects inherit from an + # existing type, or from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer - # def type_cast(value) + # def cast(value) # if value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f - # price_in_dollars * 100 + # super(price_in_dollars * 100) # else - # value.to_i + # super # end # end # end # + # # config/initializers/types.rb + # ActiveRecord::Type.register(:money, MoneyType) + # + # # /app/models/store_listing.rb # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, MoneyType.new + # attribute :price_in_cents, :money # end # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 - def attribute(name, cast_type, options = {}) + # + # For more details on creating custom types, see the documentation for + # ActiveRecord::Type::Value. For more details on registering your types + # to be referenced by a symbol, see ActiveRecord::Type.register. You can + # also pass a type object directly, in place of a symbol. + # + # ==== Querying + # + # When ActiveRecord::QueryMethods#where is called, it will + # use the type defined by the model class to convert the value to SQL, + # calling +serialize+ on your type object. For example: + # + # class Money < Struct.new(:amount, :currency) + # end + # + # class MoneyType < Type::Value + # def initialize(currency_converter) + # @currency_converter = currency_converter + # end + # + # # value will be the result of +deserialize+ or + # # +cast+. Assumed to be an instance of +Money+ in + # # this case. + # def serialize(value) + # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value) + # value_in_bitcoins.amount + # end + # end + # + # ActiveRecord::Type.register(:money, MoneyType) + # + # class Product < ActiveRecord::Base + # currency_converter = ConversionRatesFromTheInternet.new + # attribute :price_in_bitcoins, :money, currency_converter + # end + # + # Product.where(price_in_bitcoins: Money.new(5, "USD")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230 + # + # Product.where(price_in_bitcoins: Money.new(5, "GBP")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412 + # + # ==== Dirty Tracking + # + # The type of an attribute is given the opportunity to change how dirty + # tracking is performed. The methods +changed?+ and +changed_in_place?+ + # will be called from ActiveModel::Dirty. See the documentation for those + # methods in ActiveRecord::Type::Value for more details. + def attribute(name, cast_type, **options) name = name.to_s - clear_caches_calculated_from_columns - # Assign a new hash to ensure that subclasses do not share a hash - self.user_provided_columns = user_provided_columns.merge(name => cast_type) - - if options.key?(:default) - self.user_provided_defaults = user_provided_defaults.merge(name => options[:default]) - end - end + reload_schema_from_cache - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + self.attributes_to_define_after_schema_loads = + attributes_to_define_after_schema_loads.merge( + name => [cast_type, options] + ) end - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + # This is the low level API which sits beneath +attribute+. It only + # accepts type objects, and will do its work immediately instead of + # waiting for the schema to load. Automatic schema detection and + # ClassMethods#attribute both call this under the hood. While this method + # is provided so it can be used by plugin authors, application code + # should probably use ClassMethods#attribute. + # + # +name+ The name of the attribute being defined. Expected to be a +String+. + # + # +cast_type+ The type object to use for this attribute. + # + # +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+. 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+. + def define_attribute( + name, + cast_type, + default: NO_DEFAULT_PROVIDED, + user_provided_default: true + ) + attribute_types[name] = cast_type + define_default_attribute(name, default, cast_type, from_user: user_provided_default) end - def reset_column_information # :nodoc: + def load_schema! # :nodoc: super - clear_caches_calculated_from_columns - end - - private - - def add_user_provided_columns(schema_columns) - existing_columns = schema_columns.map do |column| - new_type = user_provided_columns[column.name] - if new_type - column.with_type(new_type) - else - column + attributes_to_define_after_schema_loads.each do |name, (type, options)| + if type.is_a?(Symbol) + type = ActiveRecord::Type.lookup(type, **options.except(:default)) end - end - existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)| - connection.new_column(name, nil, type) + define_attribute(name, type, **options.slice(:default)) end - - existing_columns + new_columns end - def clear_caches_calculated_from_columns - @arel_table = nil - @attributes_builder = nil - @column_names = nil - @column_types = nil - @columns = nil - @columns_hash = nil - @content_columns = nil - @default_attributes = nil - end + private + + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED - def raw_default_values - super.merge(user_provided_defaults) + def define_default_attribute(name, value, type, from_user:) + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + elsif from_user + default_attribute = Attribute::UserProvidedDefault.new( + name, + value, + type, + _default_attributes[name], + ) + else + default_attribute = Attribute.from_database(name, value, type) + end + _default_attributes[name] = default_attribute end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index fa6c5e9e8c..0792d19c3e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -177,10 +177,8 @@ module ActiveRecord # before actually defining them. def add_autosave_association_callbacks(reflection) save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - if collection + if reflection.collection? before_save :before_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } @@ -204,8 +202,18 @@ module ActiveRecord before_save save_method end + define_autosave_validation_callbacks(reflection) + end + + def define_autosave_validation_callbacks(reflection) + validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) + if reflection.collection? + method = :validate_collection_association + else + method = :validate_single_association + end + define_non_cyclic_method(validation_method) do send(method, reflection) # TODO: remove the following line as soon as the return value of @@ -278,11 +286,18 @@ module ActiveRecord # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class._reflections.values.any? do |reflection| - if reflection.options[:autosave] - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any?(&:changed_for_autosave?) + @_nested_records_changed_for_autosave_already_called ||= false + return false if @_nested_records_changed_for_autosave_already_called + begin + @_nested_records_changed_for_autosave_already_called = true + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any?(&:changed_for_autosave?) + end end + ensure + @_nested_records_changed_for_autosave_already_called = false end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 100d3780f6..c918e88590 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -5,7 +5,6 @@ require 'active_support/dependencies' require 'active_support/descendants_tracker' require 'active_support/time' require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' @@ -120,23 +119,22 @@ module ActiveRecord #:nodoc: # All column values are automatically available through basic accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting # the default accessors (using the same name as the attribute) and calling - # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually - # change things. + # +super+ to actually change things. # # class Song < ActiveRecord::Base # # Uses an integer of seconds to hold the length of the song # # def length=(minutes) - # write_attribute(:length, minutes.to_i * 60) + # super(minutes.to_i * 60) # end # # def length - # read_attribute(:length) / 60 + # super / 60 # end # end # # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. + # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. # # == Attribute query methods # @@ -258,7 +256,7 @@ module ActiveRecord #:nodoc: # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal # nothing was found, please check its documentation for further details. @@ -309,10 +307,12 @@ module ActiveRecord #:nodoc: include Aggregations include Transactions include NoTouching + include TouchLater include Reflection include Serialization include Store include SecureToken + include Suppressor end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..3027ce928e 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -194,7 +194,7 @@ module ActiveRecord # # If the +before_validation+ callback throws +:abort+, the process will be # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a - # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object. + # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object. # # == Canceling callbacks # @@ -289,25 +289,24 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private - def create_or_update(*) #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def _create_record #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def _update_record(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } 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 1371317e3c..6535121075 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,7 +1,6 @@ require 'thread' require 'thread_safe' require 'monitor' -require 'set' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -10,6 +9,12 @@ module ActiveRecord class ConnectionTimeoutError < ConnectionNotEstablished end + # Raised when a pool was unable to get ahold of all its connections + # to perform a "group" action such as +ConnectionPool#disconnect!+ + # or +ConnectionPool#clear_reloadable_connections!+. + class ExclusiveConnectionTimeoutError < ConnectionTimeoutError + end + module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -63,6 +68,15 @@ module ActiveRecord # connection at the end of a thread or a thread dies unexpectedly. # Regardless of this setting, the Reaper will be invoked before every # blocking wait. (Default nil, which means don't schedule the Reaper). + # + #-- + # Synchronization policy: + # * all public methods can be called outside +synchronize+ + # * access to these i-vars needs to be in +synchronize+: + # * @connections + # * @now_connecting + # * private methods that require being called in a +synchronize+ blocks + # are now explicitly documented class ConnectionPool # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool # with which it shares a Monitor. But could be a generic Queue. @@ -129,17 +143,15 @@ module ActiveRecord # - ConnectionTimeoutError if +timeout+ is given and no element # becomes available within +timeout+ seconds, def poll(timeout = nil) - synchronize do - if timeout - no_wait_poll || wait_poll(timeout) - else - no_wait_poll - end - end + synchronize { internal_poll(timeout) } end private + def internal_poll(timeout) + no_wait_poll || (timeout && wait_poll(timeout)) + end + def synchronize(&block) @lock.synchronize(&block) end @@ -193,6 +205,80 @@ module ActiveRecord end end + # Adds the ability to turn a basic fair FIFO queue into one + # biased to some thread. + module BiasableQueue # :nodoc: + class BiasedConditionVariable # :nodoc: + # semantics of condition variables guarantee that +broadcast+, +broadcast_on_biased+, + # +signal+ and +wait+ methods are only called while holding a lock + def initialize(lock, other_cond, preferred_thread) + @real_cond = lock.new_cond + @other_cond = other_cond + @preferred_thread = preferred_thread + @num_waiting_on_real_cond = 0 + end + + def broadcast + broadcast_on_biased + @other_cond.broadcast + end + + def broadcast_on_biased + @num_waiting_on_real_cond = 0 + @real_cond.broadcast + end + + def signal + if @num_waiting_on_real_cond > 0 + @num_waiting_on_real_cond -= 1 + @real_cond + else + @other_cond + end.signal + end + + def wait(timeout) + if Thread.current == @preferred_thread + @num_waiting_on_real_cond += 1 + @real_cond + else + @other_cond + end.wait(timeout) + end + end + + def with_a_bias_for(thread) + previous_cond = nil + new_cond = nil + synchronize do + previous_cond = @cond + @cond = new_cond = BiasedConditionVariable.new(@lock, @cond, thread) + end + yield + ensure + synchronize do + @cond = previous_cond if previous_cond + new_cond.broadcast_on_biased if new_cond # wake up any remaining sleepers + end + end + end + + # Connections must be leased while holding the main pool mutex. This is + # an internal subclass that also +.leases+ returned connections while + # still in queue's critical section (queue synchronizes with the same + # +@lock+ as the main pool) so that a returned connection is already + # leased and there is no need to re-enter synchronized block. + class ConnectionLeasingQueue < Queue # :nodoc: + include BiasableQueue + + private + def internal_poll(timeout) + conn = super + conn.lease if conn + conn + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -220,7 +306,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :checkout_timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -241,56 +327,75 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - # The cache of reserved connections mapped to threads - @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size) + # The cache of threads mapped to reserved connections, the sole purpose + # of the cache is to speed-up +connection+ method, it is not the authoritative + # registry of which thread owns which connection, that is tracked by + # +connection.owner+ attr on each +connection+ instance. + # The invariant works like this: if there is mapping of +thread => conn+, + # then that +thread+ does indeed own that +conn+, however an absence of a such + # mapping does not mean that the +thread+ doesn't own the said connection, in + # that case +conn.owner+ attr should be consulted. + # Access and modification of +@thread_cached_conns+ does not require + # synchronization. + @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size) @connections = [] @automatic_reconnect = true - @available = Queue.new self + # Connection pool allows for concurrent (outside the main `synchronize` section) + # establishment of new connections. This variable tracks the number of threads + # currently in the process of independently establishing connections to the DB. + @now_connecting = 0 + + # A boolean toggle that allows/disallows new connections. + @new_cons_enabled = true + + @available = ConnectionLeasingQueue.new self end # Retrieve the connection associated with the current thread, or call # #checkout to obtain one if necessary. # # #connection can be called any number of times; the connection is - # held in a hash keyed by the thread id. + # held in a cache keyed by a thread. def connection - # this is correctly done double-checked locking - # (ThreadSafe::Cache's lookups have volatile semantics) - @reserved_connections[current_connection_id] || synchronize do - @reserved_connections[current_connection_id] ||= checkout - end + @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout end # Is there an open connection that is being used for the current thread? + # + # This method only works for connections that have been abtained through + # #connection or #with_connection methods, connections obtained through + # #checkout will not be detected by #active_connection? def active_connection? - synchronize do - @reserved_connections.fetch(current_connection_id) { - return false - }.in_use? - end + @thread_cached_conns[connection_cache_key(Thread.current)] end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. - def release_connection(with_id = current_connection_id) - synchronize do - conn = @reserved_connections.delete(with_id) - checkin conn if conn + # + # This method only works for connections that have been obtained through + # #connection or #with_connection methods, connections obtained through + # #checkout will not be automatically released. + def release_connection(owner_thread = Thread.current) + if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread)) + checkin conn end end - # If a connection already exists yield it to the block. If no connection + # If a connection obtained through #connection or #with_connection methods + # already exists yield it to the block. If no such connection # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection - connection_id = current_connection_id - fresh_connection = true unless active_connection? - yield connection + unless conn = @thread_cached_conns[connection_cache_key(Thread.current)] + conn = connection + fresh_connection = true + end + yield conn ensure - release_connection(connection_id) if fresh_connection + release_connection if fresh_connection end # Returns true if a connection has already been opened. @@ -299,32 +404,81 @@ module ActiveRecord end # Disconnects all connections in the pool, and clears the pool. - def disconnect! - synchronize do - @reserved_connections.clear - @connections.each do |conn| - checkin conn - conn.disconnect! + # + # Raises: + # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # connections in the pool within a timeout interval (default duration is + # +spec.config[:checkout_timeout] * 2+ seconds). + def disconnect(raise_on_acquisition_timeout = true) + with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do + synchronize do + @connections.each do |conn| + checkin conn + conn.disconnect! + end + @connections = [] + @available.clear end - @connections = [] - @available.clear end end - # Clears the cache which maps classes. - def clear_reloadable_connections! - synchronize do - @reserved_connections.clear - @connections.each do |conn| - checkin conn - conn.disconnect! if conn.requires_reloading? - end - @connections.delete_if(&:requires_reloading?) - @available.clear - @connections.each do |conn| - @available.add conn + # Disconnects all connections in the pool, and clears the pool. + # + # The pool first tries to gain ownership of all connections, if unable to + # do so within a timeout interval (default duration is + # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully + # disconneted wihout any regard for other connection owning threads. + def disconnect! + disconnect(false) + end + + # Clears the cache which maps classes and re-connects connections that + # require reloading. + # + # Raises: + # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # connections in the pool within a timeout interval (default duration is + # +spec.config[:checkout_timeout] * 2+ seconds). + def clear_reloadable_connections(raise_on_acquisition_timeout = true) + num_new_conns_required = 0 + + with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do + synchronize do + @connections.each do |conn| + checkin conn + conn.disconnect! if conn.requires_reloading? + end + @connections.delete_if(&:requires_reloading?) + + @available.clear + + if @connections.size < @size + # because of the pruning done by this method, we might be running + # low on connections, while threads stuck in queue are helpless + # (not being able to establish new connections for themselves), + # see also more detailed explanation in +remove+ + num_new_conns_required = num_waiting_in_queue - @connections.size + end + + @connections.each do |conn| + @available.add conn + end end end + + bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 + end + + # Clears the cache which maps classes and re-connects connections that + # require reloading. + # + # The pool first tries to gain ownership of all connections, if unable to + # do so within a timeout interval (default duration is + # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully + # clears the cache and reloads connections without any regard for other + # connection owning threads. + def clear_reloadable_connections! + clear_reloadable_connections(false) end # Check-out a database connection from the pool, indicating that you want @@ -341,12 +495,8 @@ module ActiveRecord # # Raises: # - ConnectionTimeoutError: no connection can be obtained from the pool. - def checkout - synchronize do - conn = acquire_connection - conn.lease - checkout_and_verify(conn) - end + def checkout(checkout_timeout = @checkout_timeout) + checkout_and_verify(acquire_connection(checkout_timeout)) end # Check-in a database connection back into the pool, indicating that you @@ -356,14 +506,12 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) synchronize do - owner = conn.owner + remove_connection_from_thread_cache conn - conn._run_checkin_callbacks do + conn.run_callbacks :checkin do conn.expire end - release conn, owner - @available.add conn end end @@ -371,14 +519,32 @@ module ActiveRecord # Remove a connection from the connection pool. The connection will # remain open and active but will no longer be managed by this pool. def remove(conn) + needs_new_connection = false + synchronize do + remove_connection_from_thread_cache conn + @connections.delete conn @available.delete conn - release conn, conn.owner - - @available.add checkout_new_connection if @available.any_waiting? + # @available.any_waiting? => true means that prior to removing this + # conn, the pool was at its max size (@connections.size == @size) + # this would mean that any threads stuck waiting in the queue wouldn't + # know they could checkout_new_connection, so let's do it for them. + # Because condition-wait loop is encapsulated in the Queue class + # (that in turn is oblivious to ConnectionPool implementation), threads + # that are "stuck" there are helpless, they have no way of creating + # new connections and are completely reliant on us feeding available + # connections into the Queue. + needs_new_connection = @available.any_waiting? end + + # This is intentionally done outside of the synchronized section as we + # would like not to hold the main mutex while checking out new connections, + # thus there is some chance that needs_new_connection information is now + # stale, we can live with that (bulk_make_new_connections will make + # sure not to exceed the pool's @size limit). + bulk_make_new_connections(1) if needs_new_connection end # Recover lost connections for the pool. A lost connection can occur if @@ -403,7 +569,118 @@ module ActiveRecord end end + def num_waiting_in_queue # :nodoc: + @available.num_waiting + end + private + #-- + # this is unfortunately not concurrent + def bulk_make_new_connections(num_new_conns_needed) + num_new_conns_needed.times do + # try_to_checkout_new_connection will not exceed pool's @size limit + if new_conn = try_to_checkout_new_connection + # make the new_conn available to the starving threads stuck @available Queue + checkin(new_conn) + end + end + end + + #-- + # From the discussion on Github: + # https://github.com/rails/rails/pull/14938#commitcomment-6601951 + # This hook-in method allows for easier monkey-patching fixes needed by + # JRuby users that use Fibers. + def connection_cache_key(thread) + thread + end + + # Take control of all existing connections so a "group" action such as + # reload/disconnect can be performed safely. It is no longer enough to + # wrap it in +synchronize+ because some pool's actions are allowed + # to be performed outside of the main +synchronize+ block. + def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) + with_new_connections_blocked do + attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) + yield + end + end + + def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true) + collected_conns = synchronize do + # account for our own connections + @connections.select {|conn| conn.owner == Thread.current} + end + + newly_checked_out = [] + timeout_time = Time.now + (@checkout_timeout * 2) + + @available.with_a_bias_for(Thread.current) do + while true + synchronize do + return if collected_conns.size == @connections.size && @now_connecting == 0 + remaining_timeout = timeout_time - Time.now + remaining_timeout = 0 if remaining_timeout < 0 + conn = checkout_for_exclusive_access(remaining_timeout) + collected_conns << conn + newly_checked_out << conn + end + end + end + rescue ExclusiveConnectionTimeoutError + # `raise_on_acquisition_timeout == false` means we are directed to ignore any + # timeouts and are expected to just give up: we've obtained as many connections + # as possible, note that in a case like that we don't return any of the + # `newly_checked_out` connections. + + if raise_on_acquisition_timeout + release_newly_checked_out = true + raise + end + rescue Exception # if something else went wrong + # this can't be a "naked" rescue, because we have should return conns + # even for non-StandardErrors + release_newly_checked_out = true + raise + ensure + if release_newly_checked_out && newly_checked_out + # releasing only those conns that were checked out in this method, conns + # checked outside this method (before it was called) are not for us to release + newly_checked_out.each {|conn| checkin(conn)} + end + end + + #-- + # Must be called in a synchronize block. + def checkout_for_exclusive_access(checkout_timeout) + checkout(checkout_timeout) + rescue ConnectionTimeoutError + # this block can't be easily moved into attempt_to_checkout_all_existing_connections's + # rescue block, because doing so would put it outside of synchronize section, without + # being in a critical section thread_report might become inaccurate + msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds" + + thread_report = [] + @connections.each do |conn| + unless conn.owner == Thread.current + thread_report << "#{conn} is owned by #{conn.owner}" + end + end + + msg << " (#{thread_report.join(', ')})" if thread_report.any? + + raise ExclusiveConnectionTimeoutError, msg + end + + def with_new_connections_blocked + previous_value = nil + synchronize do + previous_value, @new_cons_enabled = @new_cons_enabled, false + end + yield + ensure + synchronize { @new_cons_enabled = previous_value } + end # Acquire a connection by one of 1) immediately removing one # from the queue of available connections, 2) creating a new @@ -412,47 +689,89 @@ module ActiveRecord # # Raises: # - ConnectionTimeoutError if a connection could not be acquired - def acquire_connection - if conn = @available.poll + # + #-- + # Implementation detail: the connection returned by +acquire_connection+ + # will already be "+connection.lease+ -ed" to the current thread. + def acquire_connection(checkout_timeout) + # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to + # `conn.lease` the returned connection (and to do this in a `synchronized` + # section), this is not the cleanest implementation, as ideally we would + # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll` + # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections + # of the said methods and avoid an additional `synchronize` overhead. + if conn = @available.poll || try_to_checkout_new_connection conn - elsif @connections.size < @size - checkout_new_connection else reap - @available.poll(@checkout_timeout) + @available.poll(checkout_timeout) end end - def release(conn, owner) - thread_id = owner.object_id - - if @reserved_connections[thread_id] == conn - @reserved_connections.delete thread_id - end + #-- + # if owner_thread param is omitted, this must be called in synchronize block + def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) + @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn) end + alias_method :release, :remove_connection_from_thread_cache def new_connection - Base.send(spec.adapter_method, spec.config) + Base.send(spec.adapter_method, spec.config).tap do |conn| + conn.schema_cache = schema_cache.dup if schema_cache + end + end + + # If the pool is not at a +@size+ limit, establish new connection. Connecting + # to the DB is done outside main synchronized section. + #-- + # Implementation constraint: a newly established connection returned by this + # method must be in the +.leased+ state. + def try_to_checkout_new_connection + # first in synchronized section check if establishing new conns is allowed + # and increment @now_connecting, to prevent overstepping this pool's @size + # constraint + do_checkout = synchronize do + if @new_cons_enabled && (@connections.size + @now_connecting) < @size + @now_connecting += 1 + end + end + if do_checkout + begin + # if successfully incremented @now_connecting establish new connection + # outside of synchronized section + conn = checkout_new_connection + ensure + synchronize do + if conn + adopt_connection(conn) + # returned conn needs to be already leased + conn.lease + end + @now_connecting -= 1 + end + end + end end - def current_connection_id #:nodoc: - Base.connection_id ||= Thread.current.object_id + def adopt_connection(conn) + conn.pool = self + @connections << conn end def checkout_new_connection raise ConnectionNotEstablished unless @automatic_reconnect - - c = new_connection - c.pool = self - @connections << c - c + new_connection end def checkout_and_verify(c) - c._run_checkout_callbacks do + c.run_callbacks :checkout do c.verify! end c + rescue + remove c + c.disconnect! + raise end end @@ -614,7 +933,9 @@ module ActiveRecord # A connection was established in an ancestor process that must have # subsequently forked. We can't reuse the connection, but we can copy # the specification and establish a new connection with it. - establish_connection owner, ancestor_pool.spec + establish_connection(owner, ancestor_pool.spec).tap do |pool| + pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache + end else owner_to_pool[owner.name] = nil end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index c0a2111571..30b2fca2ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -18,9 +18,9 @@ module ActiveRecord end # Returns the maximum allowed length for an index name. This - # limit is enforced by rails and Is less than or equal to + # limit is enforced by \Rails and is less than or equal to # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal rails + # <tt>index_name_length</tt> is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length 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 59cdd8e98c..38dd9578fe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -40,8 +40,9 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - if result = select_one(arel, name, binds) - result.values.first + arel, binds = binds_from_relation arel, binds + if result = select_rows(to_sql(arel, binds), name, binds).first + result.first end end @@ -136,7 +137,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -188,8 +189,8 @@ module ActiveRecord # You should consult the documentation for your database to understand the # semantics of these different levels: # - # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html # # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: # @@ -201,16 +202,14 @@ module ActiveRecord # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. - def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable, :isolation - - if !options[:requires_new] && current_transaction.joinable? - if options[:isolation] + def transaction(requires_new: nil, isolation: nil, joinable: true) + if !requires_new && current_transaction.joinable? + if isolation raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end yield else - transaction_manager.within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed @@ -234,6 +233,10 @@ module ActiveRecord current_transaction.add_record(record) end + def transaction_state + current_transaction.state + end + # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -285,10 +288,17 @@ module ActiveRecord def insert_fixture(fixture, table_name) columns = schema_cache.columns_hash(table_name) - key_list = [] - value_list = fixture.map do |name, value| - key_list << quote_column_name(name) - quote(value, columns[name]) + binds = fixture.map do |name, value| + type = lookup_cast_type_from_column(columns[name]) + Relation::QueryAttribute.new(name, value, type) + end + key_list = fixture.keys.map { |name| quote_column_name(name) } + value_list = prepare_binds_for_database(binds).map do |value| + begin + quote(value) + rescue TypeError + quote(YAML.dump(value)) + end end execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' @@ -375,7 +385,7 @@ module ActiveRecord def binds_from_relation(relation, binds) if relation.is_a?(Relation) && binds.empty? - relation, binds = relation.arel, relation.bind_values + relation, binds = relation.arel, relation.bound_attributes end [relation, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 143d7d9574..2c7409b2dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,7 +10,13 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) if column - value = column.cast_type.type_cast_for_database(value) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a column to `quote` has been deprecated. It is only used + for type casting, which should be handled elsewhere. See + https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 + for more information. + MSG + value = type_cast_from_column(column, value) end _quote(value) @@ -25,7 +31,7 @@ module ActiveRecord end if column - value = column.cast_type.type_cast_for_database(value) + value = type_cast_from_column(column, value) end _type_cast(value) @@ -34,10 +40,44 @@ module ActiveRecord raise TypeError, "can't cast #{value.class}#{to_type}" end + # If you are having to call this function, you are likely doing something + # wrong. The column does not have sufficient type information if the user + # provided a custom type on the class level either explicitly (via + # `attribute`) or implicitly (via `serialize`, + # `time_zone_aware_attributes`). In almost all cases, the sql type should + # only be used to change quoting behavior, when the primitive to + # represent the type doesn't sufficiently reflect the differences + # (varchar vs binary) for example. The type used to get this primitive + # should have been provided before reaching the connection adapter. + def type_cast_from_column(column, value) # :nodoc: + if column + type = lookup_cast_type_from_column(column) + type.serialize(value) + else + value + end + end + + # See docs for +type_cast_from_column+ + def lookup_cast_type_from_column(column) # :nodoc: + lookup_cast_type(column.sql_type) + end + + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. @@ -62,6 +102,11 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end + def quote_default_expression(value, column) #:nodoc: + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end + def quoted_true "'t'" end @@ -78,6 +123,8 @@ module ActiveRecord 'f' end + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -87,7 +134,16 @@ module ActiveRecord end end - value.to_s(:db) + result = value.to_s(:db) + if value.respond_to?(:usec) && value.usec > 0 + "#{result}.#{sprintf("%06d", value.usec)}" + else + result + end + end + + def prepare_binds_for_database(binds) # :nodoc: + binds.map(&:value_for_database) end private @@ -109,8 +165,7 @@ module ActiveRecord when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" - else - "'#{quote_string(YAML.dump(value))}'" + else raise TypeError, "can't quote #{value.class.name}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index db20b60d60..18d943f452 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,26 +14,29 @@ module ActiveRecord send m, o end - def visit_AddColumn(o) - "ADD #{accept(o)}" - end + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql private def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.adds.map { |col| accept col }.join(' ') sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end + def visit_AddColumnDefinition(o) + "ADD #{accept(o.column)}" + end + def visit_TableDefinition(o) create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " create_sql << "#{quote_table_name(o.name)} " @@ -67,21 +70,10 @@ module ActiveRecord column_options[:after] = o.after column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key + column_options[:collation] = o.collation column_options end - def quote_column_name(name) - @conn.quote_column_name name - end - - def quote_table_name(name) - @conn.quote_table_name name - end - - def type_to_sql(type, limit, precision, scale) - @conn.type_to_sql type.to_sym, limit, precision, scale - end - def add_column_options!(sql, options) sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations @@ -97,11 +89,6 @@ module ActiveRecord sql end - def quote_default_expression(value, column) - value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value) - end - def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end @@ -118,10 +105,6 @@ module ActiveRecord MSG end end - - def type_for_column(column) - @conn.lookup_cast_type(column.sql_type) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 7eaa89c9a7..158b773e11 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,8 +1,3 @@ -require 'date' -require 'set' -require 'bigdecimal' -require 'bigdecimal/util' - module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of @@ -15,13 +10,16 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end + class AddColumnDefinition < Struct.new(:column) # :nodoc: + end + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end @@ -50,6 +48,14 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def defined_for?(options_or_to_table = {}) + if options_or_to_table.is_a?(Hash) + options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + else + to_table == options_or_to_table.to_s + end + end + private def default_primary_key "id" @@ -130,7 +136,42 @@ module ActiveRecord end def foreign_table_name - name.to_s.pluralize + Base.pluralize_table_names ? name.to_s.pluralize : name + end + end + + module ColumnMethods + # Appends a primary key definition to the table definition. + # Can be called multiple times, but this is probably not a good idea. + def primary_key(name, type = :primary_key, **options) + column(name, type, options.merge(primary_key: true)) + end + + # Appends a column or columns of a specified type. + # + # t.string(:goat) + # t.string(:goat, :sheep) + # + # See TableDefinition#column + [ + :bigint, + :binary, + :boolean, + :date, + :datetime, + :decimal, + :float, + :integer, + :string, + :text, + :time, + :timestamp, + ].each do |column_type| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{column_type}(*args, **options) + args.each { |name| column(name, :#{column_type}, options) } + end + CODE end end @@ -155,6 +196,8 @@ module ActiveRecord # The table definitions # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition + include ColumnMethods + # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes @@ -173,12 +216,6 @@ module ActiveRecord def columns; @columns_hash.values; end - # Appends a primary key definition to the table definition. - # Can be called multiple times, but this is probably not a good idea. - def primary_key(name, type = :primary_key, options = {}) - column(name, type, options.merge(:primary_key => true)) - end - # Returns a ColumnDefinition for the column with name +name+. def [](name) @columns_hash[name.to_s] @@ -188,7 +225,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, # <tt>:binary</tt>, <tt>:boolean</tt>. # @@ -320,6 +357,7 @@ module ActiveRecord def column(name, type, options = {}) name = name.to_s type = type.to_sym + options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." @@ -335,14 +373,6 @@ module ActiveRecord @columns_hash.delete name.to_s end - [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| - define_method column_type do |*args| - options = args.extract_options! - column_names = args - column_names.each { |name| column(name, column_type, options) } - end - end - # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table # @@ -368,17 +398,12 @@ module ActiveRecord column(:updated_at, :datetime, options) end - # Adds a reference. Optionally adds a +type+ column, if the - # +:polymorphic+ option is provided. +references+ and +belongs_to+ - # are acceptable. The reference column will be an +integer+ by default, - # the +:type+ option can be used to specify a different type. A foreign - # key will be created if the +:foreign_key+ option is passed. + # Adds a reference. # # t.references(:user) - # t.references(:user, type: "string") - # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference + # See SchemaStatements#add_reference for details of the options you can use. def references(*args, **options) args.each do |col| ReferenceDefinition.new(col, **options).add_to(self) @@ -402,6 +427,7 @@ module ActiveRecord column.after = options[:after] column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] + column.collation = options[:collation] column end @@ -444,7 +470,7 @@ module ActiveRecord def add_column(name, type, options) name = name.to_s type = type.to_sym - @adds << @td.new_column_definition(name, type, options) + @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options)) end end @@ -454,6 +480,7 @@ module ActiveRecord # Available transformations are: # # change_table :table do |t| + # t.primary_key # t.column # t.index # t.rename_index @@ -466,6 +493,7 @@ module ActiveRecord # t.string # t.text # t.integer + # t.bigint # t.float # t.decimal # t.datetime @@ -482,6 +510,8 @@ module ActiveRecord # end # class Table + include ColumnMethods + attr_reader :name def initialize(table_name, base) @@ -500,6 +530,8 @@ module ActiveRecord # Checks to see if a column exists. # + # t.string(:name) unless t.column_exists?(:name, :string) + # # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) @@ -519,6 +551,10 @@ module ActiveRecord # Checks to see if an index exists. # + # unless t.index_exists?(:branch_id) + # t.index(:branch_id) + # end + # # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) @@ -601,15 +637,12 @@ module ActiveRecord @base.rename_column(name, column_name, new_column_name) end - # Adds a reference. Optionally adds a +type+ column, if - # <tt>:polymorphic</tt> option is provided. + # Adds a reference. # # t.references(:user) - # t.references(:user, type: "string") - # t.belongs_to(:supplier, polymorphic: true) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference + # See SchemaStatements#add_reference for details of the options you can use. def references(*args) options = args.extract_options! args.each do |ref_name| @@ -632,25 +665,24 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type. + # Adds a foreign key. # - # t.string(:goat) - # t.string(:goat, :sheep) + # t.foreign_key(:authors) # - # See SchemaStatements#add_column - [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| - define_method column_type do |*args| - options = args.extract_options! - args.each do |column_name| - @base.add_column(name, column_name, column_type, options) - end - end - end - + # See SchemaStatements#add_foreign_key def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end + # Checks to see if a foreign key exists. + # + # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) + # + # See SchemaStatements#foreign_key_exists? + def foreign_key_exists?(*args) # :nodoc: + @base.foreign_key_exists?(name, *args) + end + private def native @base.native_database_types diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 42ea599a74..b944a8631c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -24,33 +24,66 @@ module ActiveRecord def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect - spec[:type] = column.type.to_s + spec[:type] = schema_type(column) spec[:null] = 'false' unless column.null - limit = column.limit || native_database_types[column.type][:limit] - spec[:limit] = limit.inspect if limit - spec[:precision] = column.precision.inspect if column.precision - spec[:scale] = column.scale.inspect if column.scale + if limit = schema_limit(column) + spec[:limit] = limit + end + + if precision = schema_precision(column) + spec[:precision] = precision + end + + if scale = schema_scale(column) + spec[:scale] = scale + end default = schema_default(column) if column.has_default? spec[:default] = default unless default.nil? + if collation = schema_collation(column) + spec[:collation] = collation + end + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null] + [:name, :limit, :precision, :scale, :default, :null, :collation] end private + def schema_type(column) + column.type.to_s + end + + def schema_limit(column) + limit = column.limit || native_database_types[column.type][:limit] + limit.inspect if limit + end + + def schema_precision(column) + column.precision.inspect if column.precision + end + + def schema_scale(column) + column.scale.inspect if column.scale + end + def schema_default(column) - default = column.type_cast_from_database(column.default) + type = lookup_cast_type_from_column(column) + default = type.deserialize(column.default) unless default.nil? - column.type_cast_for_schema(default) + type.type_cast_for_schema(default) end end + + def schema_collation(column) + column.collation.inspect if column.collation + end end end end 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 0f44c332ae..c8be038d76 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,6 @@ require 'active_record/migration/join_table' +require 'active_support/core_ext/string/access' +require 'digest' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -12,6 +14,10 @@ module ActiveRecord {} end + def table_options(table_name) + nil + end + # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) table_name[0...table_alias_length].tr('.', '_') @@ -118,6 +124,8 @@ module ActiveRecord # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. # Join tables for +has_and_belongs_to_many+ should set it to false. + # + # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. @@ -161,6 +169,19 @@ module ActiveRecord # name varchar(80) # ) # + # ====== Change the primary key column type + # + # create_table(:tags, id: :string) do |t| + # t.column :label, :string + # end + # + # generates: + # + # CREATE TABLE tags ( + # id varchar PRIMARY KEY, + # label varchar + # ) + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -375,16 +396,23 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) - execute "DROP TABLE #{quote_table_name(table_name)}" + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. + # + # Note: Not all options will be available, generally this command should + # ignore most of them. In favor of doing a low-level call to simply + # create a column. def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -528,6 +556,8 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # # ====== Creating an index with a specific method # # add_index(:developers, :name, using: 'btree') @@ -619,11 +649,21 @@ module ActiveRecord indexes(table_name).detect { |i| i.name == index_name } end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify - # a different type. + # Adds a reference. The reference column is an integer by default, + # the <tt>:type</tt> option can be used to specify a different type. + # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # + # The +options+ hash can include the following keys: + # [<tt>:type</tt>] + # The reference column type. Defaults to +:integer+. + # [<tt>:index</tt>] + # Add an appropriate index. Defaults to false. + # [<tt>:foreign_key</tt>] + # Add an appropriate foreign key. Defaults to false. + # [<tt>:polymorphic</tt>] + # Whether an additional +_type+ column should be added. Defaults to false. + # # ====== Create a user_id integer column # # add_reference(:products, :user) @@ -632,10 +672,6 @@ module ActiveRecord # # add_reference(:products, :user, type: :string) # - # ====== Create a supplier_id and supplier_type columns - # - # add_belongs_to(:products, :supplier, polymorphic: true) - # # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) @@ -650,7 +686,7 @@ module ActiveRecord alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. # # ====== Remove the reference # @@ -660,7 +696,16 @@ module ActiveRecord # # remove_reference(:products, :supplier, polymorphic: true) # + # ====== Remove the reference with a foreign key + # + # remove_reference(:products, :user, index: true, foreign_key: true) + # def remove_reference(table_name, ref_name, options = {}) + if options[:foreign_key] + reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name + remove_foreign_key(table_name, reference_name) + end + remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] end @@ -676,8 +721,8 @@ module ActiveRecord # +to_table+ contains the referenced primary key. # # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. - # +identifier+ is a 10 character long random string. A custom name can be specified with - # the <tt>:name</tt> option. + # +identifier+ is a 10 character long string which is deterministically generated from the + # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option. # # ====== Creating a simple foreign key # @@ -732,7 +777,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+. # @@ -746,24 +794,11 @@ 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? - if options_or_to_table.is_a?(Hash) - options = options_or_to_table - else - options = { column: foreign_key_column_for(options_or_to_table) } - end - - fk_name_to_delete = options.fetch(:name) do - fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s } - - if fk_to_delete - fk_to_delete.name - else - raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" - end - end + fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name at = create_alter_table from_table at.drop_foreign_key fk_name_to_delete @@ -771,6 +806,31 @@ module ActiveRecord execute schema_creation.accept(at) end + # Checks to see if a foreign key exists on a table for a given foreign key definition. + # + # # Check a foreign key exists + # foreign_key_exists?(:accounts, :branches) + # + # # Check a foreign key on a specified column exists + # foreign_key_exists?(:accounts, column: :owner_id) + # + # # Check a foreign key with a custom name exists + # foreign_key_exists?(:accounts, name: "special_fk_name") + # + def foreign_key_exists?(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table).present? + end + + def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: + return unless supports_foreign_keys? + foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: + foreign_key_for(from_table, options_or_to_table) or \ + raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + end + def foreign_key_column_for(table_name) # :nodoc: "#{table_name.to_s.singularize}_id" end @@ -832,6 +892,12 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end + elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + if (0..6) === precision + column_type_sql << "(#{precision})" + else + raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit]) column_type_sql << "(#{limit})" end @@ -878,13 +944,13 @@ module ActiveRecord def add_index_options(table_name, column_name, options = {}) #:nodoc: column_names = Array(column_name) - index_name = index_name(table_name, column: column_names) options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - index_type = options[:unique] ? "UNIQUE" : "" index_type = options[:type].to_s if options.key?(:type) + index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + index_name ||= index_name(table_name, column: column_names) max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if options.key?(:algorithm) @@ -990,12 +1056,14 @@ module ActiveRecord end def foreign_key_name(table_name, options) # :nodoc: + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do - "fk_rails_#{SecureRandom.hex(5)}" + "fk_rails_#{hashed_identifier}" end end - def validate_index_length!(table_name, new_name) + def validate_index_length!(table_name, new_name) # :nodoc: if new_name.length > allowed_index_name_length raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index f6ef3b0675..295a7bed87 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,13 +1,10 @@ module ActiveRecord module ConnectionAdapters class TransactionState - attr_reader :parent - VALID_STATES = Set.new([:committed, :rolledback, nil]) def initialize(state = nil) @state = state - @parent = nil end def finalized? @@ -27,7 +24,7 @@ module ActiveRecord end def set_state(state) - if !VALID_STATES.include?(state) + unless VALID_STATES.include?(state) raise ArgumentError, "Invalid transaction state: #{state}" end @state = state @@ -47,19 +44,16 @@ module ActiveRecord attr_reader :connection, :state, :records, :savepoint_name attr_writer :joinable - def initialize(connection, options) + def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] @joinable = options.fetch(:joinable, true) + @run_commit_callbacks = run_commit_callbacks end def add_record(record) - if record.has_transactional_callbacks? - records << record - else - record.set_transaction_state(@state) - end + records << record end def rollback @@ -69,11 +63,11 @@ module ActiveRecord def rollback_records ite = records.uniq while record = ite.shift - record.rolledback! full_rollback? + record.rolledback!(force_restore_state: full_rollback?) end ensure ite.each do |i| - i.rolledback!(full_rollback?, false) + i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false) end end @@ -81,15 +75,22 @@ module ActiveRecord @state.set_state(:committed) end + def before_commit_records + records.uniq.each(&:before_committed!) if @run_commit_callbacks + end + def commit_records ite = records.uniq while record = ite.shift - record.committed! + if @run_commit_callbacks + record.committed! + else + # if not running callbacks, only adds the record to the parent transaction + record.add_to_transaction + end end ensure - ite.each do |i| - i.committed!(false) - end + ite.each { |i| i.committed!(should_run_callbacks: false) } end def full_rollback?; true; end @@ -100,8 +101,8 @@ module ActiveRecord class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, options) - super(connection, options) + def initialize(connection, savepoint_name, options, *args) + super(connection, options, *args) if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end @@ -111,14 +112,11 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) super - rollback_records end def commit connection.release_savepoint(savepoint_name) super - parent = connection.transaction_manager.current_transaction - records.each { |r| parent.add_record(r) } end def full_rollback?; false; end @@ -126,7 +124,7 @@ module ActiveRecord class RealTransaction < Transaction - def initialize(connection, options) + def initialize(connection, options, *args) super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) @@ -138,13 +136,11 @@ module ActiveRecord def rollback connection.rollback_db_transaction super - rollback_records end def commit connection.commit_db_transaction super - commit_records end end @@ -155,22 +151,31 @@ module ActiveRecord end def begin_transaction(options = {}) + run_commit_callbacks = !current_transaction.joinable? transaction = if @stack.empty? - RealTransaction.new(@connection, options) + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) end + @stack.push(transaction) transaction end def commit_transaction - @stack.pop.commit + transaction = @stack.last + transaction.before_commit_records + @stack.pop + transaction.commit + transaction.commit_records end - def rollback_transaction - @stack.pop.rollback + def rollback_transaction(transaction = nil) + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records end def within_new_transaction(options = {}) @@ -182,12 +187,12 @@ module ActiveRecord ensure unless error if Thread.current.status == 'aborting' - rollback_transaction + rollback_transaction if transaction else begin commit_transaction rescue Exception - transaction.rollback unless transaction.state.completed? + rollback_transaction(transaction) unless transaction.state.completed? raise 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 c3a8bf5c74..6d3a21a3dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,12 +1,9 @@ -require 'date' -require 'bigdecimal' -require 'bigdecimal/util' require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' -require 'monitor' require 'arel/collectors/bind' require 'arel/collectors/sql_string' @@ -69,7 +66,6 @@ module ActiveRecord include DatabaseLimits include QueryCache include ActiveSupport::Callbacks - include MonitorMixin include ColumnDumper SIMPLE_INT = /\A\d+\z/ @@ -113,7 +109,8 @@ module ActiveRecord class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) - super(bvs.map { |bv| conn.quote(*bv.reverse) }) + casted_binds = conn.prepare_binds_for_database(bvs) + super(casted_binds.map { |value| conn.quote(value) }) end end @@ -139,12 +136,20 @@ module ActiveRecord SchemaCreation.new self end + # this method must only be called while holding connection pool's mutex def lease - synchronize do - unless in_use? - @owner = Thread.current + if in_use? + msg = 'Cannot lease connection, ' + if @owner == Thread.current + msg << 'it is already leased by the current thread.' + else + msg << "it is already in use by a different thread: #{@owner}. " << + "Current thread: #{Thread.current}." end + raise ActiveRecordError, msg end + + @owner = Thread.current end def schema_cache=(cache) @@ -152,6 +157,7 @@ module ActiveRecord @schema_cache = cache end + # this method must only be called while holding connection pool's mutex def expire @owner = nil end @@ -243,6 +249,11 @@ module ActiveRecord false end + # Does this adapter support datetime with precision? + def supports_datetime_with_precision? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -261,18 +272,6 @@ module ActiveRecord {} end - # QUOTING ================================================== - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - # Returns a bind substitution value given a bind +column+ # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, _unused = 0) @@ -365,9 +364,18 @@ module ActiveRecord end def case_insensitive_comparison(table, attribute, column, value) - table[attribute].lower.eq(table.lower(value)) + if can_perform_case_insensitive_comparison_for?(column) + table[attribute].lower.eq(table.lower(value)) + else + case_sensitive_comparison(table, attribute, column, value) + end end + def can_perform_case_insensitive_comparison_for?(column) + true + end + private :can_perform_case_insensitive_comparison_for? + def current_savepoint_name current_transaction.savepoint_name end @@ -383,8 +391,8 @@ module ActiveRecord end end - def new_column(name, default, cast_type, sql_type = nil, null = true) - Column.new(name, default, cast_type, sql_type, null) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) + Column.new(name, default, sql_type_metadata, null, default_function, collation) end def lookup_cast_type(sql_type) # :nodoc: @@ -392,21 +400,21 @@ module ActiveRecord end def column_name_for_operation(operation, node) # :nodoc: - node.to_sql + visitor.accept(node, collector).value end protected def initialize_type_map(m) # :nodoc: - register_class_with_limit m, %r(boolean)i, Type::Boolean - register_class_with_limit m, %r(char)i, Type::String - register_class_with_limit m, %r(binary)i, Type::Binary - register_class_with_limit m, %r(text)i, Type::Text - register_class_with_limit m, %r(date)i, Type::Date - register_class_with_limit m, %r(time)i, Type::Time - register_class_with_limit m, %r(datetime)i, Type::DateTime - register_class_with_limit m, %r(float)i, Type::Float - register_class_with_limit m, %r(int)i, Type::Integer + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_precision m, %r(date)i, Type::Date + register_class_with_precision m, %r(time)i, Type::Time + register_class_with_precision m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer m.alias_type %r(blob)i, 'binary' m.alias_type %r(clob)i, 'text' @@ -440,6 +448,13 @@ module ActiveRecord end end + def register_class_with_precision(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + precision = extract_precision(args.last) + klass.new(precision: precision) + end + end + def extract_scale(sql_type) # :nodoc: case sql_type when /\((\d+)\)/ then 0 @@ -452,7 +467,12 @@ module ActiveRecord end def extract_limit(sql_type) # :nodoc: - $1.to_i if sql_type =~ /\((.*)\)/ + case sql_type + when /^bigint/i + 8 + when /\((.*)\)/ + $1.to_i + end end def translate_exception_class(e, sql) @@ -462,7 +482,6 @@ module ActiveRecord message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" end - @logger.error message if @logger exception = translate_exception(e, message) exception.set_backtrace e.backtrace exception 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 e9a3c26c32..00e3d2965b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,3 @@ -require 'arel/visitors/bind_visitor' require 'active_support/core_ext/string/strip' module ActiveRecord @@ -6,20 +5,45 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include Savepoints - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - def primary_key(name, type = :primary_key, options = {}) - options[:auto_increment] ||= type == :bigint + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint super end end - class SchemaCreation < AbstractAdapter::SchemaCreation - def visit_AddColumn(o) - add_column_position!(super, column_options(o)) + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + end + column.charset = options[:charset] + column end private + def create_column_definition(name, type) + ColumnDefinition.new(name, type) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + + class SchemaCreation < AbstractAdapter::SchemaCreation + private + def visit_DropForeignKey(name) "DROP FOREIGN KEY #{name}" end @@ -37,11 +61,31 @@ module ActiveRecord create_sql end + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + def visit_ChangeColumnDefinition(o) change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" add_column_position!(change_column_sql, column_options(o.column)) end + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + def add_column_position!(sql, options) if options[:first] sql << " FIRST" @@ -52,20 +96,24 @@ module ActiveRecord end def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" + index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " end end + def update_table_definition(table_name, base) # :nodoc: + Table.new(table_name, base) + end + def schema_creation SchemaCreation.new self end def column_spec_for_primary_key(column) spec = {} - if column.extra == 'auto_increment' - return unless column.limit == 8 - spec[:id] = ':bigint' + if column.auto_increment? + spec[:id] = ':bigint' if column.bigint? + return if spec.empty? else spec[:id] = column.type.inspect spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) @@ -73,14 +121,31 @@ module ActiveRecord spec end + private + + def schema_limit(column) + super unless column.type == :boolean + end + + def schema_precision(column) + super unless /time/ === column.sql_type && column.precision == 0 + end + + def schema_collation(column) + if column.collation && table_name = column.instance_variable_get(:@table_name) + @collation_cache ||= {} + @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + column.collation.inspect if column.collation != @collation_cache[table_name] + end + end + + public + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict, :extra + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true - def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") - @strict = strict - @collation = collation - @extra = extra - super(name, default, cast_type, sql_type, null) + def initialize(*) + super assert_valid_default(default) extract_default end @@ -88,7 +153,7 @@ module ActiveRecord def extract_default if blob_or_text_column? @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@default) + elsif missing_default_forged_as_empty_string?(default) @default = nil end end @@ -106,11 +171,8 @@ module ActiveRecord collation && !collation.match(/_ci$/) end - def ==(other) - super && - collation == other.collation && - strict == other.strict && - extra == other.extra + def auto_increment? + extra == 'auto_increment' end private @@ -131,9 +193,32 @@ module ActiveRecord raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end end + end + + class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :extra, :strict + + def initialize(type_metadata, extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MysqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected def attributes_for_hash - super + [collation, strict, extra] + [self.class, @type_metadata, extra, strict] end end @@ -188,6 +273,16 @@ module ActiveRecord end end + MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191 + CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32'] + def initialize_schema_migrations_table + if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) + else + ActiveRecord::SchemaMigration.create_table + end + end + # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -227,6 +322,10 @@ module ActiveRecord version[0] >= 5 end + def supports_datetime_with_precision? + (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -243,8 +342,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) + def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + Column.new(field, default, sql_type_metadata, null, default_function, collation) end # Must return the MySQL error number from the exception, if the exception has an @@ -467,8 +566,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - cast_type = lookup_cast_type(sql_type) - new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) + type_metadata = fetch_type_metadata(sql_type, field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) end end end @@ -501,8 +600,23 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + # Drops a table from the database. + # + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. + # [<tt>:temporary</tt>] + # Set to +true+ to drop temporary table. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end def rename_index(table_name, old_name, new_name) @@ -520,7 +634,7 @@ module ActiveRecord change_column table_name, column_name, column.sql_type, :default => default end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: column = column_for(table_name, column_name) unless null || default.nil? @@ -540,8 +654,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" end def foreign_keys(table_name) @@ -572,40 +686,25 @@ module ActiveRecord end end + def table_options(table_name) + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + # strip create_definitions and partition_options + raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + + # strip AUTO_INCREMENT + raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s when 'binary' - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") - end + binary_to_sql(limit) when 'integer' - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end + integer_to_sql(limit) when 'text' - case limit - when 0..0xff; 'tinytext' - when nil, 0x100..0xffff; 'text' - when 0x10000..0xffffff; 'mediumtext' - when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") - end - when 'datetime' - return super unless precision - - case precision - when 0..6; "datetime(#{precision})" - else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.") - end + text_to_sql(limit) else super end @@ -694,11 +793,6 @@ module ActiveRecord m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' - m.register_type(%r(datetime)i) do |sql_type| - precision = extract_precision(sql_type) - MysqlDateTime.new(precision: precision) - end - m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] .split(',').map{|enum| enum.strip.length - 2}.max @@ -716,15 +810,16 @@ module ActiveRecord end end - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] + def extract_precision(sql_type) + if /time/ === sql_type + super || 0 + else + super + end + end - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - subselect.from subsubselect.as('__active_record_temp') + def fetch_type_metadata(sql_type, extra = "") + MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end def add_index_length(option_strings, column_names, options = {}) @@ -766,7 +861,7 @@ module ActiveRecord def add_column_sql(table_name, column_name, type, options = {}) td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) - schema_creation.visit_AddColumn cd + schema_creation.accept(AddColumnDefinition.new(cd)) end def change_column_sql(table_name, column_name, type, options = {}) @@ -790,7 +885,7 @@ module ActiveRecord options = { default: column.default, null: column.null, - auto_increment: column.extra == "auto_increment" + auto_increment: column.auto_increment? } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] @@ -808,8 +903,9 @@ module ActiveRecord end def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + index_algorithm[0, 0] = ", " if index_algorithm.present? + "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end def remove_index_sql(table_name, options = {}) @@ -827,6 +923,19 @@ module ActiveRecord private + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + # Materialized subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.from subsubselect.distinct.as('__active_record_temp') + end + def version @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) end @@ -842,8 +951,7 @@ module ActiveRecord def configure_connection variables = @config.fetch(:variables, {}).stringify_keys - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. @@ -851,15 +959,17 @@ module ActiveRecord wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) + defaults = [':default', :default].to_set + # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - unless variables.has_key?('sql_mode') + unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict]) variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" @@ -869,7 +979,7 @@ module ActiveRecord # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| - if v == ':default' || v == :default + if defaults.include?(v) "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? "@@SESSION.#{k} = #{quote(v)}" @@ -894,24 +1004,38 @@ module ActiveRecord TableDefinition.new(native_database_types, name, temporary, options, as) end - class MysqlDateTime < Type::DateTime # :nodoc: - def type_cast_for_database(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - result = super.to_s(:db) - case precision - when 1..6 - "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" - else - result - end - else - super - end + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xfff; "varbinary(#{limit})" + when nil; "blob" + when 0x1000..0xffffffff; "blob(#{limit})" + else raise(ActiveRecordError, "No binary type has character length #{limit}") + end + end + + def integer_to_sql(limit) # :nodoc: + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def text_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has character length #{limit}") end end class MysqlString < Type::String # :nodoc: - def type_cast_for_database(value) + def serialize(value) case value when true then "1" when false then "0" @@ -929,6 +1053,9 @@ module ActiveRecord end end end + + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index e74de60a83..4b95b0681d 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,34 +12,32 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation - delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :changed?, - :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, - :type_cast_for_schema, - to: :cast_type + delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +cast_type+ is the object used for type casting and type information. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - @name = name - @cast_type = cast_type - @sql_type = sql_type - @null = null - @default = default + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) + @name = name + @sql_type_metadata = sql_type_metadata + @null = null + @default = default @default_function = default_function + @collation = collation + @table_name = nil end def has_default? - !default.nil? + !default.nil? || default_function + end + + def bigint? + /bigint/ === sql_type end # Returns the human name of the column name. @@ -50,19 +48,9 @@ module ActiveRecord Base.human_attribute_name(@name) end - def with_type(type) - dup.tap do |clone| - clone.instance_variable_set('@cast_type', type) - end - end - def ==(other) - other.name == name && - other.default == default && - other.cast_type == cast_type && - other.sql_type == sql_type && - other.null == null && - other.default_function == default_function + other.is_a?(Column) && + attributes_for_hash == other.attributes_for_hash end alias :eql? :== @@ -70,16 +58,16 @@ module ActiveRecord attributes_for_hash.hash end - private + protected def attributes_for_hash - [self.class, name, default, cast_type, sql_type, null, default_function] + [self.class, name, default, sql_type_metadata, null, default_function, collation] end end class NullColumn < Column def initialize(name) - super name, nil, Type::Value.new + super(name, nil) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 75f244b3f3..e97e82f056 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.13' +gem 'mysql2', '~> 0.3.18' require 'mysql2' module ActiveRecord @@ -37,15 +37,6 @@ module ActiveRecord configure_connection end - MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 - def initialize_schema_migrations_table - if @config[:encoding] == 'utf8mb4' - ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) - else - ActiveRecord::SchemaMigration.create_table - end - end - def supports_explain? true end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 23d8389abb..2ae462d773 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -5,8 +5,10 @@ require 'active_support/core_ext/hash/keys' gem 'mysql', '~> 2.9' require 'mysql' -class Mysql +class Mysql # :nodoc: all class Time + # Used for casting DateTime fields to a MySQL friendly Time. + # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e def to_date Date.new(year, month, day) end @@ -56,9 +58,9 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -69,34 +71,10 @@ module ActiveRecord ADAPTER_NAME = 'MySQL'.freeze class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max = 1000) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - def delete(key); cache.delete(key); end - - def []=(sql, key) - while @max <= cache.size - cache.shift.last[:stmt].close - end - cache[sql] = key - end - - def clear - cache.each_value do |hash| - hash[:stmt].close - end - cache.clear - end - private - def cache - @cache[Process.pid] + + def dealloc(stmt) + stmt[:stmt].close end end @@ -328,8 +306,8 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super - m.register_type %r(datetime)i, Fields::DateTime.new - m.register_type %r(time)i, Fields::Time.new + register_class_with_precision m, %r(datetime)i, Fields::DateTime + register_class_with_precision m, %r(time)i, Fields::Time end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -395,11 +373,9 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -410,14 +386,17 @@ module ActiveRecord end begin - stmt.execute(*type_casted_binds.map { |_, val| val }) + stmt.execute(*type_casted_binds) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. - stmt.close - @statements.delete sql + if binds.empty? + stmt.close + else + @statements.delete sql + end raise e end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb deleted file mode 100644 index 1b74c039ce..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ /dev/null @@ -1,93 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module ArrayParser # :nodoc: - - DOUBLE_QUOTE = '"' - BACKSLASH = "\\" - COMMA = ',' - BRACKET_OPEN = '{' - BRACKET_CLOSE = '}' - - def parse_pg_array(string) # :nodoc: - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end - - array - end - - private - - def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false - current_item = '' - - local_index = index - while local_index - token = string[local_index] - if is_escaping - current_item << token - is_escaping = false - else - if is_quoted - case token - when DOUBLE_QUOTE - is_quoted = false - was_quoted = true - when BACKSLASH - is_escaping = true - else - current_item << token - end - else - case token - when BACKSLASH - is_escaping = true - when COMMA - add_item_to_array(array, current_item, was_quoted) - current_item = '' - was_quoted = false - when DOUBLE_QUOTE - is_quoted = true - when BRACKET_OPEN - internal_items = [] - local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) - array.push(internal_items) - when BRACKET_CLOSE - add_item_to_array(array, current_item, was_quoted) - return local_index,array - else - current_item << token - end - end - end - - local_index += 1 - end - return local_index,array - end - - def add_item_to_array(array, current_item, quoted) - return if !quoted && current_item.length == 0 - - if !quoted && current_item == 'NULL' - array.push nil - else - array.push current_item - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index acb1278499..bfa03fa136 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -2,21 +2,14 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - attr_reader :array + delegate :array, :oid, :fmod, to: :sql_type_metadata alias :array? :array - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - if sql_type =~ /\[\]$/ - @array = true - sql_type = sql_type[0..sql_type.length - 3] - else - @array = false - end - super - end - def serial? - default_function && default_function =~ /\Anextval\(.*\)\z/ + return unless default_function + + table_name = @table_name || '(?<table_name>.+)' + %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index d28a2b4fa0..68752cdd80 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,20 @@ -require 'active_record/connection_adapters/postgresql/oid/infinity' - require 'active_record/connection_adapters/postgresql/oid/array' require 'active_record/connection_adapters/postgresql/oid/bit' require 'active_record/connection_adapters/postgresql/oid/bit_varying' require 'active_record/connection_adapters/postgresql/oid/bytea' require 'active_record/connection_adapters/postgresql/oid/cidr' -require 'active_record/connection_adapters/postgresql/oid/date' require 'active_record/connection_adapters/postgresql/oid/date_time' require 'active_record/connection_adapters/postgresql/oid/decimal' require 'active_record/connection_adapters/postgresql/oid/enum' -require 'active_record/connection_adapters/postgresql/oid/float' require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' -require 'active_record/connection_adapters/postgresql/oid/integer' 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/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' require 'active_record/connection_adapters/postgresql/oid/xml' 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 c203e6c604..25961a9869 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -3,51 +3,53 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Mutable - - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end + include Type::Helpers::Mutable attr_reader :subtype, :delimiter - delegate :type, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter + + @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter + @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :type_cast_from_database) + type_cast_array(@pg_decoder.decode(value), :deserialize) else super end end - def type_cast_from_user(value) + def cast(value) if value.is_a?(::String) - value = parse_pg_array(value) + value = @pg_decoder.decode(value) end - type_cast_array(value, :type_cast_from_user) + type_cast_array(value, :cast) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) - cast_value_for_database(value) + @pg_encoder.encode(type_cast_array(value, :serialize)) else super end end + def ==(other) + other.is_a?(Array) && + subtype == other.subtype && + 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) @@ -57,41 +59,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def cast_value_for_database(value) - if value.is_a?(::Array) - casted_values = value.map { |item| cast_value_for_database(item) } - "{#{casted_values.join(delimiter)}}" - else - quote_and_escape(subtype.type_cast_for_database(value)) - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when ::String - if string_requires_quoting?(value) - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - %("#{value}") - else - value - end - when nil then "NULL" - else value - end - end - - # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO - # for a list of all cases in which strings will be quoted. - def string_requires_quoting?(string) - string.empty? || - string == "NULL" || - string =~ /[\{\}"\\\s]/ || - string.include?(delimiter) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 1dbb40ca1d..ea0fa2517f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -7,7 +7,7 @@ module ActiveRecord :bit end - def type_cast(value) + def cast(value) if ::String === value case value when /^0x/i @@ -20,7 +20,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) Data.new(super) if value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 6bd1b8ecae..8f9d6e7f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Bytea < Type::Binary # :nodoc: - def type_cast_from_database(value) + def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 222f10fa8f..eeccb09bdf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -18,7 +18,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if IPAddr === value "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb deleted file mode 100644 index 1d8d264530..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Date < Type::Date # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index b9e7894e5c..2c04c46131 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: - include Infinity - def cast_value(value) if value.is_a?(::String) case value diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 77d5038efd..91d339f32c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -7,7 +7,9 @@ module ActiveRecord :enum end - def type_cast(value) + private + + def cast_value(value) value.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb deleted file mode 100644 index 78ef94b912..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Float < Type::Float # :nodoc: - include Infinity - - def cast_value(value) - case value - when ::Float then value - when 'Infinity' then ::Float::INFINITY - when '-Infinity' then -::Float::INFINITY - when 'NaN' then ::Float::NAN - else value.to_f - end - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index be4525c94f..9270fc9f21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -3,13 +3,13 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :hstore end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') @@ -21,7 +21,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Hash) value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb deleted file mode 100644 index e47780399a..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - module Infinity # :nodoc: - def infinity(options = {}) - options[:negative] ? -::Float::INFINITY : ::Float::INFINITY - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb deleted file mode 100644 index 59abdc0009..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Integer < Type::Integer # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index e12ddd9901..8e1256baad 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -3,25 +3,25 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Json < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :json end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) + ::ActiveSupport::JSON.decode(value) rescue nil else - super + value end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) || value.is_a?(::Hash) ::ActiveSupport::JSON.encode(value) else - super + value end end 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 380c50fc14..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,11 +9,11 @@ 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. - raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + raw_old_value = serialize(deserialize(raw_old_value)) super(raw_old_value, new_value) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index df890c2ed6..2163674019 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Money < Type::Decimal # :nodoc: - include Infinity - class_attribute :precision def type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index bac8b01d6b..bf565bcf47 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -3,19 +3,19 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Point < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :point end - def type_cast(value) + def cast(value) case value when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - type_cast(value.split(',')) + cast(value.split(',')) when ::Array value.map { |v| Float(v) } else @@ -23,7 +23,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else 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/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 3adfb8b9d8..fc201f8fb9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -7,7 +7,7 @@ module ActiveRecord class Range < Type::Value # :nodoc: attr_reader :subtype, :type - def initialize(subtype, type) + def initialize(subtype, type = :range) @subtype = subtype @type = type end @@ -30,7 +30,7 @@ module ActiveRecord ::Range.new(from, to, extracted[:exclude_end]) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) @@ -40,26 +40,42 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Range) && + other.subtype == subtype && + other.type == type + end + private def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) + infinity?(value) ? value : @subtype.deserialize(value) end def type_cast_single_for_database(value) - infinity?(value) ? '' : @subtype.type_cast_for_database(value) + infinity?(value) ? '' : @subtype.serialize(value) end def extract_bounds(value) from, to = value[1..-2].split(',') { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? infinity : to, exclude_start: (value[0] == '('), exclude_end: (value[-1] == ')') } end + def infinity(negative: false) + if subtype.respond_to?(:infinity) + subtype.infinity(negative: negative) + elsif negative + -::Float::INFINITY + else + ::Float::INFINITY + end + end + def infinity?(value) value.respond_to?(:infinite?) && value.infinite? end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index b2a42e9ebb..2d2fede4e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -8,10 +8,6 @@ module ActiveRecord def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb deleted file mode 100644 index 8f0246eddb..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Time < Type::Time # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 9b3de41fab..191c828e60 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -15,11 +15,11 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row['oid'].to_i } mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } + composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } @@ -29,6 +29,18 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end + def query_conditions_for_initial_load(type_map) + known_type_names = type_map.keys.map { |n| "'#{n}'" } + known_type_types = %w('r' 'e' 'd') + <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + WHERE + t.typname IN (%s) + OR t.typtype IN (%s) + OR t.typinput::varchar = 'array_in' + OR t.typelem != 0 + SQL + end + private def register_mapped_type(row) alias_type row['oid'], row['typname'] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 97b4fd3d08..5e839228e9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -5,13 +5,13 @@ module ActiveRecord class Uuid < Type::Value # :nodoc: ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x - alias_method :type_cast_for_database, :type_cast_from_database + alias_method :serialize, :deserialize def type :uuid end - def type_cast(value) + def cast(value) value.to_s[ACCEPTABLE_UUID, 0] end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index de4187b028..b26e876b54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -16,7 +16,7 @@ module ActiveRecord # FIXME: this should probably split on +delim+ and use +subtype+ # to cast the values. Unfortunately, the current Rails behavior # is to just return the string. - def type_cast(value) + def cast(value) value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb index 334af7c598..d40d837cee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -7,7 +7,7 @@ module ActiveRecord :xml end - def type_cast_for_database(value) + def serialize(value) return unless value Data.new(super) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 9de9e2c7dc..f175730551 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -40,8 +40,7 @@ module ActiveRecord PGconn.quote_ident(name.to_s) end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. + # Quote date/time values for use in SQL input. def quoted_date(value) #:nodoc: if value.year <= 0 bce_year = format("%04d", -value.year + 1) @@ -52,15 +51,21 @@ module ActiveRecord end # Does not quote function default values for UUID columns - def quote_default_value(value, column) #:nodoc: + def quote_default_expression(value, column) #:nodoc: if column.type == :uuid && value =~ /\(\)/ value - else - value = column.cast_type.type_cast_for_database(value) + elsif column.respond_to?(:array?) + value = type_cast_from_column(column, value) quote(value) + else + super end end + def lookup_cast_type_from_column(column) # :nodoc: + type_map.lookup(column.oid, column.fmod, column.sql_type) + end + private def _quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index 52b307c432..44a7338bf5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -8,20 +8,39 @@ module ActiveRecord def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? + original_exception = nil + begin - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - rescue - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";")) + transaction(requires_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + rescue ActiveRecord::ActiveRecordError => e + original_exception = e end - end - yield - ensure - if supports_disable_referential_integrity? + + begin + yield + rescue ActiveRecord::InvalidForeignKey => e + warn <<-WARNING +WARNING: Rails was not able to disable referential integrity. + +This is most likely caused due to missing permissions. +Rails needs superuser privileges to disable referential integrity. + + cause: #{original_exception.try(:message)} + + WARNING + raise e + end + begin - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - rescue - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";")) + transaction(requires_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + rescue ActiveRecord::ActiveRecordError end + else + yield 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 a9522e152f..022dbdfa27 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -2,90 +2,129 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], :xml, options) + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, **options) + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super + end + + def bigserial(*args, **options) + args.each { |name| column(name, :bigserial, options) } + end + + def bit(*args, **options) + args.each { |name| column(name, :bit, options) } end - def tsvector(*args) - options = args.extract_options! - column(args[0], :tsvector, options) + def bit_varying(*args, **options) + args.each { |name| column(name, :bit_varying, options) } end - def int4range(name, options = {}) - column(name, :int4range, options) + def cidr(*args, **options) + args.each { |name| column(name, :cidr, options) } end - def int8range(name, options = {}) - column(name, :int8range, options) + def citext(*args, **options) + args.each { |name| column(name, :citext, options) } end - def tsrange(name, options = {}) - column(name, :tsrange, options) + def daterange(*args, **options) + args.each { |name| column(name, :daterange, options) } end - def tstzrange(name, options = {}) - column(name, :tstzrange, options) + def hstore(*args, **options) + args.each { |name| column(name, :hstore, options) } end - def numrange(name, options = {}) - column(name, :numrange, options) + def inet(*args, **options) + args.each { |name| column(name, :inet, options) } end - def daterange(name, options = {}) - column(name, :daterange, options) + def int4range(*args, **options) + args.each { |name| column(name, :int4range, options) } end - def hstore(name, options = {}) - column(name, :hstore, options) + def int8range(*args, **options) + args.each { |name| column(name, :int8range, options) } end - def ltree(name, options = {}) - column(name, :ltree, options) + def json(*args, **options) + args.each { |name| column(name, :json, options) } end - def inet(name, options = {}) - column(name, :inet, options) + def jsonb(*args, **options) + args.each { |name| column(name, :jsonb, options) } end - def cidr(name, options = {}) - column(name, :cidr, options) + def ltree(*args, **options) + args.each { |name| column(name, :ltree, options) } end - def macaddr(name, options = {}) - column(name, :macaddr, options) + def macaddr(*args, **options) + args.each { |name| column(name, :macaddr, options) } end - def uuid(name, options = {}) - column(name, :uuid, options) + def money(*args, **options) + args.each { |name| column(name, :money, options) } end - def json(name, options = {}) - column(name, :json, options) + def numrange(*args, **options) + args.each { |name| column(name, :numrange, options) } end - def jsonb(name, options = {}) - column(name, :jsonb, options) + def point(*args, **options) + args.each { |name| column(name, :point, options) } end - def citext(name, options = {}) - column(name, :citext, options) + def serial(*args, **options) + args.each { |name| column(name, :serial, options) } end - def point(name, options = {}) - column(name, :point, options) + def tsrange(*args, **options) + args.each { |name| column(name, :tsrange, options) } end - def bit(name, options) - column(name, :bit, options) + def tstzrange(*args, **options) + args.each { |name| column(name, :tstzrange, options) } end - def bit_varying(name, options) - column(name, :bit_varying, options) + def tsvector(*args, **options) + args.each { |name| column(name, :tsvector, options) } end - def money(name, options) - column(name, :money, options) + def uuid(*args, **options) + args.each { |name| column(name, :uuid, options) } + end + + def xml(*args, **options) + args.each { |name| column(name, :xml, options) } end end @@ -96,39 +135,6 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid - super - end - def new_column_definition(name, type, options) # :nodoc: column = super column.array = options[:array] 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 a90adcf4aa..595c635fc0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,34 +4,17 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def column_options(o) - column_options = super - column_options[:array] = o.array - column_options + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) + super end def add_column_options!(sql, options) - if options[:array] - sql << '[]' + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" end super end - - def quote_default_expression(value, column) - if column.type == :uuid && value =~ /\(\)/ - value - else - super - end - end - - def type_for_column(column) - if column.array - @conn.lookup_cast_type("#{column.sql_type}[]") - else - super - end - end end module SchemaStatements @@ -87,11 +70,7 @@ module ActiveRecord # Returns the list of all tables in the schema search path or a specified schema. def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end # Returns true if table exists. @@ -101,7 +80,7 @@ module ActiveRecord name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -111,21 +90,18 @@ module ActiveRecord SQL end - def drop_table(table_name, options = {}) - execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + def drop_table(table_name, options = {}) # :nodoc: + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = '#{name}' - SQL + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0 end + # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -153,8 +129,8 @@ module ActiveRecord result.map do |row| index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") + unique = row[1] + indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] @@ -182,53 +158,48 @@ module ActiveRecord # Returns the list of all column definitions for a table. def columns(table_name) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) - default_value = extract_value_from_default(oid, default) + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation| + oid = oid.to_i + fmod = fmod.to_i + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, oid, type, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function, collation) end end - def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: - PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation) end # Returns the current database name. def current_database - query('select current_database()', 'SCHEMA')[0][0] + select_value('select current_database()', 'SCHEMA') end # Returns the current schema name. def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] + select_value('SELECT current_schema', 'SCHEMA') end # Returns the current database encoding format. def encoding - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns the current database collation. def collation - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns the current database ctype. def ctype - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns an array of schema names. def schema_names - query(<<-SQL, 'SCHEMA').flatten + select_values(<<-SQL, 'SCHEMA') SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -261,12 +232,12 @@ module ActiveRecord # Returns the active schema search path. def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA') end # Returns the current client message level. def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] + select_value('SHOW client_min_messages', 'SCHEMA') end # Set the client message level. @@ -284,10 +255,7 @@ module ActiveRecord end def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') - SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql - result.rows.first.first + select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA') end # Sets the sequence of a table's primary key to the specified value. @@ -298,9 +266,7 @@ module ActiveRecord if sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' - SELECT setval('#{quoted_sequence}', #{value}) - end_sql + select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA') else @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger end @@ -323,7 +289,7 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' + select_value(<<-end_sql, 'SCHEMA') SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) end_sql end @@ -385,7 +351,7 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - pks = exec_query(<<-end_sql, 'SCHEMA').rows + pks = query(<<-end_sql, 'SCHEMA') SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) @@ -417,23 +383,25 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) + def add_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! super end - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) + def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! quoted_table_name = quote_table_name(table_name) - sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) - sql_type << "[]" if options[:array] - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" - sql << " USING #{options[:using]}" if options[:using] - if options[:cast_as] - sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})" + quoted_column_name = quote_column_name(column_name) + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array]) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" end execute sql @@ -442,7 +410,7 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column @@ -453,21 +421,21 @@ module ActiveRecord # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". execute alter_column_query % "DROP DEFAULT" else - execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) + def rename_column(table_name, column_name, new_column_name) #:nodoc: clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" rename_column_indexes(table_name, column_name, new_column_name) @@ -482,6 +450,8 @@ module ActiveRecord execute "DROP INDEX #{quote_table_name(index_name)}" end + # Renames an index of a table. Raises error if length of new + # index name is greater than allowed limit. def rename_index(table_name, old_name, new_name) validate_index_length!(table_name, new_name) @@ -530,41 +500,35 @@ module ActiveRecord end # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s + def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) + sql = case type.to_s when 'binary' # PostgreSQL doesn't support limits on binary (bytea) columns. - # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + # The hard limit is 1GB, because of a 32-bit size field, and TOAST. case limit when nil, 0..0x3fffffff; super(type) else raise(ActiveRecordError, "No binary type has byte size #{limit}.") end when 'text' # PostgreSQL doesn't support limits on text columns. - # The hard limit is 1Gb, according to section 8.3 in the manual. + # The hard limit is 1GB, according to section 8.3 in the manual. case limit when nil, 0..0x3fffffff; super(type) else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") end when 'integer' - return 'integer' unless limit - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") - end - when 'datetime' - return super unless precision - - case precision - when 0..6; "timestamp(#{precision})" - else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + when 1, 2; 'smallint' + when nil, 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end else - super + super(type, limit, precision, scale) end + + sql << '[]' if array && type != :primary_key + sql end # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and @@ -580,6 +544,18 @@ module ActiveRecord [super, *order_columns].join(', ') end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb new file mode 100644 index 0000000000..58715978f7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + attr_reader :oid, :fmod, :array + + def initialize(type_metadata, oid: nil, fmod: nil) + super(type_metadata) + @type_metadata = type_metadata + @oid = oid + @fmod = fmod + @array = /\[\]$/ === type_metadata.sql_type + end + + def sql_type + super.gsub(/\[\]$/, "") + end + + def ==(other) + other.is_a?(PostgreSQLTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, oid, fmod] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5b070cae4f..2c43c46a3d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,21 +1,19 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' - -require 'active_record/connection_adapters/postgresql/utils' -require 'active_record/connection_adapters/postgresql/column' -require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/quoting' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/schema_definitions' -require 'active_record/connection_adapters/postgresql/schema_statements' -require 'active_record/connection_adapters/postgresql/database_statements' - -require 'arel/visitors/bind_visitor' - -# Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.15' +# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility +gem 'pg', '~> 0.18' require 'pg' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/postgresql/column" +require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/oid" +require "active_record/connection_adapters/postgresql/quoting" +require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_statements" +require "active_record/connection_adapters/postgresql/type_metadata" +require "active_record/connection_adapters/postgresql/utils" +require "active_record/connection_adapters/statement_pool" + require 'ipaddr' module ActiveRecord @@ -64,15 +62,15 @@ module ActiveRecord # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. # * <tt>:variables</tt> - An optional hash of additional parameters that # will be used in <tt>SET SESSION key = val</tt> calls on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use of <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See - # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the + # http://www.postgresql.org/docs/current/static/libpq-connect.html for the # list of parameters. # # In addition, default connection parameters of libpq can be set per environment variables. - # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . + # See http://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = 'PostgreSQL'.freeze @@ -128,7 +126,7 @@ module ActiveRecord def column_spec_for_primary_key(column) spec = {} if column.serial? - return unless column.sql_type == 'bigint' + return unless column.bigint? spec[:id] = ':bigserial' elsif column.type == :uuid spec[:id] = ':uuid' @@ -145,7 +143,6 @@ module ActiveRecord def prepare_column_options(column) # :nodoc: spec = super spec[:array] = 'true' if column.array? - spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end @@ -154,6 +151,26 @@ module ActiveRecord super + [:array] end + def schema_type(column) + return super unless column.serial? + + if column.bigint? + 'bigserial' + else + 'serial' + end + end + private :schema_type + + def schema_default(column) + if column.default_function + column.default_function.inspect unless column.serial? + else + super + end + end + private :schema_default + # Returns +true+, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -180,6 +197,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -188,44 +209,18 @@ module ActiveRecord def initialize(connection, max) super @counter = 0 - @cache = Hash.new { |h,pid| h[pid] = {} } end - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - def next_key "a#{@counter + 1}" end def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last) - end - @counter += 1 - cache[sql] = key - end - - def clear - cache.each_value do |stmt_key| - dealloc stmt_key - end - cache.clear - end - - def delete(sql_key) - dealloc cache[sql_key] - cache.delete sql_key + super.tap { @counter += 1 } end private - def cache - @cache[Process.pid] - end - def dealloc(key) @connection.query "DEALLOCATE #{key}" if connection_active? end @@ -255,6 +250,7 @@ module ActiveRecord @table_alias_length = nil connect + add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -262,6 +258,8 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + add_pg_decoders + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @@ -426,7 +424,7 @@ module ActiveRecord @connection.server_version end - # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" @@ -459,11 +457,11 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', OID::Integer - register_class_with_limit m, 'int4', OID::Integer - register_class_with_limit m, 'int8', OID::Integer + register_class_with_limit m, 'int2', Type::Integer + register_class_with_limit m, 'int4', Type::Integer + register_class_with_limit m, 'int8', Type::Integer m.alias_type 'oid', 'int2' - m.register_type 'float4', OID::Float.new + m.register_type 'float4', Type::Float.new m.alias_type 'float8', 'float4' m.register_type 'text', Type::Text.new register_class_with_limit m, 'varchar', Type::String @@ -474,8 +472,7 @@ module ActiveRecord register_class_with_limit m, 'bit', OID::Bit register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' - m.register_type 'date', OID::Date.new - m.register_type 'time', OID::Time.new + m.register_type 'date', Type::Date.new m.register_type 'money', OID::Money.new m.register_type 'bytea', OID::Bytea.new @@ -501,10 +498,8 @@ module ActiveRecord m.alias_type 'lseg', 'varchar' m.alias_type 'box', 'varchar' - m.register_type 'timestamp' do |_, _, sql_type| - precision = extract_precision(sql_type) - OID::DateTime.new(precision: precision) - end + register_class_with_precision m, 'time', Type::Time + register_class_with_precision m, 'timestamp', OID::DateTime m.register_type 'numeric' do |_, fmod, sql_type| precision = extract_precision(sql_type) @@ -541,13 +536,13 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(oid, default) # :nodoc: + def extract_value_from_default(default) # :nodoc: case default # Quoted types when /\A[\(B]?'(.*)'::/m - $1.gsub(/''/, "'") + $1.gsub("''".freeze, "'".freeze) # Boolean types - when 'true', 'false' + when 'true'.freeze, 'false'.freeze default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -571,6 +566,8 @@ module ActiveRecord end def load_additional_types(type_map, oids = nil) # :nodoc: + initializer = OID::TypeMapInitializer.new(type_map) + if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -586,11 +583,13 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") + else + query += initializer.query_conditions_for_initial_load(type_map) end - initializer = OID::TypeMapInitializer.new(type_map) - records = execute(query, 'SCHEMA') - initializer.run(records) + execute_and_clear(query, 'SCHEMA', []) do |records| + initializer.run(records) + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -609,12 +608,10 @@ module ActiveRecord def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds, stmt_key) do - @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val }) + log(sql, name, binds, stmt_key) do + @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e pgerror = e.original_exception @@ -701,7 +698,7 @@ module ActiveRecord end # SET statements from :variables config hash - # http://www.postgresql.org/docs/8.3/static/sql-set.html + # http://www.postgresql.org/docs/current/static/sql-set.html variables = @config[:variables] || {} variables.map do |k, v| if v == ':default' || v == :default @@ -745,9 +742,11 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) # :nodoc: - exec_query(<<-end_sql, 'SCHEMA').rows + query(<<-end_sql, 'SCHEMA') SELECT a.attname, format_type(a.atttypid, a.atttypmod), - pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, + (SELECT c.collname FROM pg_collation c, pg_type t + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass @@ -764,6 +763,86 @@ module ActiveRecord def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end + + def can_perform_case_insensitive_comparison_for?(column) + @case_insensitive_cache ||= {} + @case_insensitive_cache[column.sql_type] ||= begin + sql = <<-end_sql + SELECT exists( + SELECT * FROM pg_proc + INNER JOIN pg_cast + ON casttarget::text::oidvector = proargtypes + WHERE proname = 'lower' + AND castsource = '#{column.sql_type}'::regtype::oid + ) + end_sql + execute_and_clear(sql, "SCHEMA", []) do |result| + result.getvalue(0, 0) + end + end + end + + def add_pg_encoders + map = PG::TypeMapByClass.new + map[Integer] = PG::TextEncoder::Integer.new + map[TrueClass] = PG::TextEncoder::Boolean.new + map[FalseClass] = PG::TextEncoder::Boolean.new + map[Float] = PG::TextEncoder::Float.new + @connection.type_map_for_queries = map + end + + def add_pg_decoders + coders_by_name = { + 'int2' => PG::TextDecoder::Integer, + 'int4' => PG::TextDecoder::Integer, + 'int8' => PG::TextDecoder::Integer, + 'oid' => PG::TextDecoder::Integer, + 'float4' => PG::TextDecoder::Float, + 'float8' => PG::TextDecoder::Float, + 'bool' => PG::TextDecoder::Boolean, + } + known_coder_types = coders_by_name.keys.map { |n| quote(n) } + query = <<-SQL % known_coder_types.join(", ") + SELECT t.oid, t.typname + FROM pg_type as t + WHERE t.typname IN (%s) + SQL + coders = execute_and_clear(query, "SCHEMA", []) do |result| + result + .map { |row| construct_coder(row, coders_by_name[row['typname']]) } + .compact + end + + map = PG::TypeMapByOid.new + coders.each { |coder| map.add_coder(coder) } + @connection.type_map_for_results = map + end + + def construct_coder(row, coder_class) + return unless coder_class + coder_class.new(oid: row['oid'].to_i, name: row['typname']) + end + + ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) + ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql) + ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgresql) + ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) + ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) + ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql) + ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) + ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) + ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) + ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) + ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) + 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) end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 37ff4e4613..981d5d7a3c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -13,6 +13,14 @@ module ActiveRecord @tables = {} end + def initialize_dup(other) + super + @columns = @columns.dup + @columns_hash = @columns_hash.dup + @primary_keys = @primary_keys.dup + @tables = @tables.dup + end + def primary_keys(table_name) @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb new file mode 100644 index 0000000000..ccb7e154ee --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + class SqlTypeMetadata + attr_reader :sql_type, :type, :limit, :precision, :scale + + def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil) + @sql_type = sql_type + @type = type + @limit = limit + @precision = precision + @scale = scale + end + + def ==(other) + other.is_a?(SqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, sql_type, type, limit, precision, scale] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb new file mode 100644 index 0000000000..fe1dcbd710 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class SchemaCreation < AbstractAdapter::SchemaCreation + private + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 03dfd29a0a..87129c42cf 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' -require 'arel/visitors/bind_visitor' +require 'active_record/connection_adapters/sqlite3/schema_creation' gem 'sqlite3', '~> 1.3.6' require 'sqlite3' @@ -41,25 +41,6 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Binary < Type::Binary # :nodoc: - def cast_value(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - - class SQLite3String < Type::String # :nodoc: - def type_cast_for_database(value) - if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT - value.encode(Encoding::UTF_8) - else - super - end - end - end - # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -97,40 +78,17 @@ module ActiveRecord end class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.each_value do |hash| - dealloc hash[:stmt] - end - cache.clear - end - private - def cache - @cache[$$] - end def dealloc(stmt) - stmt.close unless stmt.closed? + stmt[:stmt].close unless stmt[:stmt].closed? end end + def schema_creation # :nodoc: + SQLite3::SchemaCreation.new self + end + def initialize(connection, logger, connection_options, config) super(connection, logger) @@ -140,6 +98,7 @@ module ActiveRecord @config = config @visitor = Arel::Visitors::SQLite.new self + @quoted_column_names = {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -239,6 +198,12 @@ module ActiveRecord case value when BigDecimal value.to_f + when String + if value.encoding == Encoding::ASCII_8BIT + super(value.encode(Encoding::UTF_8)) + else + super + end else super end @@ -253,7 +218,7 @@ module ActiveRecord end def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") + @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") end #-- @@ -280,11 +245,9 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) @@ -302,7 +265,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds.map { |_, val| val } + stmt.bind_params type_casted_binds end ActiveRecord::Result.new(cols, stmt.to_a) @@ -386,9 +349,10 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end + collation = field['collation'] sql_type = field['type'] - cast_type = lookup_cast_type(sql_type) - new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + type_metadata = fetch_type_metadata(sql_type) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation) end end @@ -464,7 +428,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: unless null || default.nil? exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end @@ -483,6 +447,7 @@ module ActiveRecord self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) self.scale = options[:scale] if options.include?(:scale) + self.collation = options[:collation] if options.include?(:collation) end end end @@ -495,16 +460,10 @@ module ActiveRecord protected - def initialize_type_map(m) - super - m.register_type(/binary/i, SQLite3Binary.new) - register_class_with_limit m, %r(char)i, SQLite3String - end - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA') raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure + table_structure_with_collation(table_name, structure) end def alter_table(table_name, options = {}) #:nodoc: @@ -539,7 +498,7 @@ module ActiveRecord @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, :precision => column.precision, :scale => column.scale, - :null => column.null) + :null => column.null, collation: column.collation) end yield @definition if block_given? end @@ -601,6 +560,46 @@ module ActiveRecord super end end + + private + COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze + + def table_structure_with_collation(table_name, basic_structure) + collation_hash = {} + sql = "SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type='table' and name='#{ table_name }' \;" + + # Result will have following sample string + # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + # "password_digest" varchar COLLATE "NOCASE"); + result = exec_query(sql, 'SCHEMA').first + + if result + # Splitting with left parantheses and picking up last will return all + # columns separated with comma(,). + columns_string = result["sql"].split('(').last + + columns_string.split(',').each do |column_string| + # This regex will match the column name and collation type and will save + # the value in $1 and $2 respectively. + collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string) + end + + basic_structure.map! do |column| + column_name = column['name'] + + if collation_hash.has_key? column_name + column['collation'] = collation_hash[column_name] + end + + column + end + else + basic_structure.to_hash + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index c6b1bc8b5b..82e9ef3d3d 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -4,35 +4,53 @@ module ActiveRecord include Enumerable def initialize(connection, max = 1000) + @cache = Hash.new { |h,pid| h[pid] = {} } @connection = connection @max = max end - def each - raise NotImplementedError + def each(&block) + cache.each(&block) end def key?(key) - raise NotImplementedError + cache.key?(key) end def [](key) - raise NotImplementedError + cache[key] end def length - raise NotImplementedError + cache.length end - def []=(sql, key) - raise NotImplementedError + def []=(sql, stmt) + while @max <= cache.size + dealloc(cache.shift.last) + end + cache[sql] = stmt end def clear - raise NotImplementedError + cache.each_value do |stmt| + dealloc stmt + end + cache.clear end def delete(key) + dealloc cache[key] + cache.delete(key) + end + + private + + def cache + @cache[Process.pid] + end + + def dealloc(stmt) raise NotImplementedError end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 984af79642..d6b661ff76 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -5,7 +5,7 @@ module ActiveRecord # Establishes the connection to the database. Accepts a hash as input where # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) - # example for regular databases (MySQL, Postgresql, etc): + # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( # adapter: "mysql", @@ -88,7 +88,7 @@ module ActiveRecord end def connection_id - ActiveRecord::RuntimeRegistry.connection_id + ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id end def connection_id=(connection_id) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 5a5139256d..8a014e682e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -85,10 +85,30 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true + ## + # :singleton-method: + # Specifies which database schemas to dump when calling db:structure:dump. + # If the value is :schema_search_path (the default), any schemas listed in + # schema_search_path are dumped. Use :all to dump all schemas regardless + # of schema_search_path, or a string of comma separated schemas for a + # custom list. + mattr_accessor :dump_schemas, instance_writer: false + self.dump_schemas = :schema_search_path + + ## + # :singleton-method: + # Specify a threshold for the size of query result sets. If the number of + # records in the set exceeds the threshold, a warning is logged. This can + # be used to identify queries which load thousands of records and + # potentially cause memory bloat. + mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false + self.warn_on_records_fetched_greater_than = nil + mattr_accessor :maintain_test_schema, instance_accessor: false + mattr_accessor :belongs_to_required_by_default, instance_accessor: false + class_attribute :default_connection_handler, instance_writer: false - class_attribute :find_by_statement_cache def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler @@ -107,24 +127,22 @@ module ActiveRecord super end - def initialize_find_by_cache - self.find_by_statement_cache = {}.extend(Mutex_m) + def initialize_find_by_cache # :nodoc: + @find_by_statement_cache = {}.extend(Mutex_m) end - def inherited(child_class) + def inherited(child_class) # :nodoc: + # initialize cache at class definition for thread safety child_class.initialize_find_by_cache super end - def find(*ids) + def find(*ids) # :nodoc: # We don't have cache keys for this stuff yet return super unless ids.length == 1 - # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5 - return super if ids.first.kind_of?(Symbol) return super if block_given? || primary_key.nil? || - default_scopes.any? || - current_scope || + scope_attributes? || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) @@ -136,14 +154,13 @@ module ActiveRecord Please pass the id of the object by calling `.id` MSG end + key = primary_key - s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { - find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| - where(key => params.bind).limit(1) - } + statement = cached_find_by_statement(key) { |params| + where(key => params.bind).limit(1) } - record = s.execute([id], self, connection).first + record = statement.execute([id], self, connection).first unless record raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" end @@ -152,9 +169,8 @@ module ActiveRecord raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end - def find_by(*args) - return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? - return super if default_scopes.any? + def find_by(*args) # :nodoc: + return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? hash = args.first @@ -165,19 +181,16 @@ module ActiveRecord # We can't cache Post.find_by(author: david) ...yet return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } - key = hash.keys + keys = hash.keys - klass = self - s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { - find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| - wheres = key.each_with_object({}) { |param,o| - o[param] = params.bind - } - klass.where(wheres).limit(1) + statement = cached_find_by_statement(keys) { |params| + wheres = keys.each_with_object({}) { |param, o| + o[param] = params.bind } + where(wheres).limit(1) } begin - s.execute(hash.values, self, connection).first + statement.execute(hash.values, self, connection).first rescue TypeError => e raise ActiveRecord::StatementInvalid.new(e.message, e) rescue RangeError @@ -185,11 +198,11 @@ module ActiveRecord end end - def find_by!(*args) + def find_by!(*args) # :nodoc: find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") end - def initialize_generated_modules + def initialize_generated_modules # :nodoc: generated_association_methods end @@ -210,7 +223,7 @@ module ActiveRecord elsif !connected? "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', ' "#{super}(#{attr_list})" else "#{super}(Table doesn't exist)" @@ -251,6 +264,12 @@ module ActiveRecord private + def cached_find_by_statement(key, &block) # :nodoc: + @find_by_statement_cache[key] || @find_by_statement_cache.synchronize { + @find_by_statement_cache[key] ||= StatementCache.create(connection, &block) + } + end + def relation # :nodoc: relation = Relation.create(self, arel_table, predicate_builder) @@ -274,32 +293,35 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil, options = {}) + def initialize(attributes = nil) @attributes = self.class._default_attributes.dup self.class.define_attribute_methods init_internals initialize_internals_callback - # +options+ argument is only needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - init_attributes(attributes, options) if attributes + assign_attributes(attributes) if attributes yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize end - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: + # Initialize an empty model object from +coder+. +coder+ should be + # the result of previously encoding an Active Record model, using + # `encode_with` # # class Post < ActiveRecord::Base # end # + # old_post = Post.new(title: "hello world") + # coder = {} + # old_post.encode_with(coder) + # # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.init_with(coder) # post.title # => 'hello world' def init_with(coder) + coder = LegacyYamlAdapter.convert(self.class, coder) @attributes = coder['attributes'] init_internals @@ -308,8 +330,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -345,10 +367,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - _run_initialize_callbacks - - @aggregation_cache = {} - @association_cache = {} + run_callbacks(:initialize) @new_record = true @destroyed = false @@ -373,6 +392,7 @@ module ActiveRecord coder['raw_attributes'] = attributes_before_type_cast coder['attributes'] = @attributes coder['new_record'] = new_record? + coder['active_record_yaml_version'] = 1 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -458,6 +478,7 @@ module ActiveRecord # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` # when pp is required. def pretty_print(pp) + return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } @@ -483,51 +504,8 @@ module ActiveRecord Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end - def set_transaction_state(state) # :nodoc: - @transaction_state = state - end - - def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? - end - private - # Updates the attributes on this particular ActiveRecord object so that - # if it is associated with a transaction, then the state of the AR object - # will be updated to reflect the current state of the transaction - # - # The @transaction_state variable stores the states of the associated - # transaction. This relies on the fact that a transaction can only be in - # one rollback or commit (otherwise a list of states would be required) - # Each AR object inside of a transaction carries that transaction's - # TransactionState. - # - # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the ActiveRecord object - # as appropriate. - # - # Since ActiveRecord objects can be inside multiple transactions, this - # method recursively goes through the parent of the TransactionState and - # checks if the ActiveRecord object reflects the state of the object. - def sync_with_transaction_state - update_attributes_from_transaction_state(@transaction_state, 0) - end - - def update_attributes_from_transaction_state(transaction_state, depth) - if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? - unless @reflects_state[depth] - restore_transaction_record_state if transaction_state.rolledback? - clear_transaction_record_state - @reflects_state[depth] = true - end - - if transaction_state.parent && !@reflects_state[depth+1] - update_attributes_from_transaction_state(transaction_state.parent, depth+1) - end - end - end - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements # of the array, and then rescues from the possible NoMethodError. If those elements are # ActiveRecord::Base's, then this triggers the various method_missing's that we have, @@ -541,8 +519,6 @@ module ActiveRecord end def init_internals - @aggregation_cache = {} - @association_cache = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -551,22 +527,19 @@ module ActiveRecord @txn = nil @_start_transaction_state = {} @transaction_state = nil - @reflects_state = [false] end def initialize_internals_callback end - # This method is needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - def init_attributes(attributes, options) - assign_attributes(attributes) - end - def thaw if frozen? @attributes = @attributes.dup end end + + def custom_inspect_method_defined? + self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 7d8e0a2063..82596b63df 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -156,7 +156,7 @@ module ActiveRecord def each_counter_cached_associations _reflections.each do |name, reflection| - yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f053372cfb..ad33c84dbc 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,6 +32,12 @@ module ActiveRecord # Conversation.active # Conversation.archived # + # Of course, you can also query them directly if the scopes don't fit your + # needs: + # + # Conversation.where(status: [:active, :archived]) + # Conversation.where.not(status: :active) + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -59,15 +65,33 @@ module ActiveRecord # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute - # name: + # name, which return the mapping in a +HashWithIndifferentAccess+: # - # Conversation.statuses # => { "active" => 0, "archived" => 1 } + # Conversation.statuses[:active] # => 0 + # Conversation.statuses["archived"] # => 1 # - # Use that class method when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum. + # For example, you can use that when manually building SQL strings: # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # - # Where conditions on an enum attribute must use the ordinal value of an enum. + # You can use <tt>:enum_prefix</tt>/<tt>:enum_suffix</tt> option then you need + # to define multiple enums with same values. If option value is <tt>true</tt>, + # the methods are prefixed/suffixed with the name of the enum. + # + # class Invoice < ActiveRecord::Base + # enum verification: [:done, :fail], enum_prefix: true + # end + # + # It is also possible to supply a custom prefix. + # + # class Invoice < ActiveRecord::Base + # enum verification: [:done, :fail], enum_prefix: :verification_status + # end + # + # Note that <tt>:enum_prefix</tt>/<tt>:enum_postfix</tt> are reserved keywords + # and can not be used as an enum name. + module Enum def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) @@ -79,8 +103,42 @@ module ActiveRecord super end + class EnumType < Type::Value + def initialize(name, mapping) + @name = name + @mapping = mapping + end + + def cast(value) + return if value.blank? + + if mapping.has_key?(value) + value.to_s + elsif mapping.has_value?(value) + mapping.key(value) + else + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + + def deserialize(value) + return if value.nil? + mapping.key(value.to_i) + end + + def serialize(value) + mapping.fetch(value, value) + end + + protected + + attr_reader :name, :mapping + end + def enum(definitions) klass = self + enum_prefix = definitions.delete(:enum_prefix) + enum_suffix = definitions.delete(:enum_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new @@ -90,45 +148,39 @@ module ActiveRecord detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } - _enum_methods_module.module_eval do - # def status=(value) self[:status] = statuses[value] end - klass.send(:detect_enum_conflict!, name, "#{name}=") - define_method("#{name}=") { |value| - if enum_values.has_key?(value) || value.blank? - self[name] = enum_values[value] - elsif enum_values.has_value?(value) - # Assigning a value directly is not a end-user feature, hence it's not documented. - # This is used internally to make building objects from the generated scopes work - # as expected, i.e. +Conversation.archived.build.archived?+ should be true. - self[name] = value - else - raise ArgumentError, "'#{value}' is not a valid #{name}" - end - } - - # def status() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, name) - define_method(name) { enum_values.key self[name] } + detect_enum_conflict!(name, name) + detect_enum_conflict!(name, "#{name}=") - # def status_before_type_cast() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") - define_method("#{name}_before_type_cast") { enum_values.key self[name] } + attribute name, EnumType.new(name, enum_values) + _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] == i } + 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 => i } + 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 @@ -138,25 +190,7 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new do - private - def save_changed_attribute(attr_name, old) - if (mapping = self.class.defined_enums[attr_name.to_s]) - value = _read_attribute(attr_name) - if attribute_changed?(attr_name) - if mapping[old] == value - clear_attribute_changes([attr_name]) - end - else - if old != value - set_attribute_was(attr_name, mapping.key(old)) - end - end - else - super - end - end - end + mod = Module.new include mod mod end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc28ab585f..0f1759abaa 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -71,9 +71,9 @@ module ActiveRecord class RecordNotDestroyed < ActiveRecordError attr_reader :record - def initialize(record) + def initialize(message, record = nil) @record = record - super() + super(message) end end @@ -179,17 +179,7 @@ module ActiveRecord end # Raised when unknown attributes are supplied via mass assignment. - class UnknownAttributeError < NoMethodError - - attr_reader :record, :attribute - - def initialize(record, attribute) - @record = record - @attribute = attribute.to_s - super("unknown attribute '#{attribute}' for #{@record.class}.") - end - - end + UnknownAttributeError = ActiveModel::UnknownAttributeError # Raised when an error occurred while doing a mass assignment to an attribute through the # +attributes=+ method. The exception has an +attribute+ property that is the name of the diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 5f6a75ebef..738fab2bd9 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,6 +1,7 @@ require 'erb' require 'yaml' require 'zlib' +require 'set' require 'active_support/dependencies' require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' @@ -131,20 +132,20 @@ module ActiveRecord # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) # end # end - # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers + # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers # # - use the helper method in a fixture # photo: # name: kitten.png # sha: <%= file_sha 'files/kitten.png' %> # - # = Transactional Fixtures + # = Transactional Tests # # Test cases can use begin+rollback to isolate their changes to the database instead of having to # delete+insert for every test case. # # class FooTest < ActiveSupport::TestCase - # self.use_transactional_fixtures = true + # self.use_transactional_tests = true # # test "godzilla" do # assert !Foo.all.empty? @@ -158,14 +159,14 @@ module ActiveRecord # end # # If you preload your test database with all fixture data (probably in the rake task) and use - # transactional fixtures, then you may omit all fixtures declarations in your test cases since + # transactional tests, then you may omit all fixtures declarations in your test cases since # all the data's already there and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to # true. This will provide access to fixture data for every table that has been loaded through # fixtures (depending on the value of +use_instantiated_fixtures+). # - # When *not* to use transactional fixtures: + # When *not* to use transactional tests: # # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until # all parent transactions commit, particularly, the fixtures transaction which is begun in setup @@ -521,12 +522,16 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map connection.transaction(:requires_new => true) do + deleted_tables = Set.new fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + unless deleted_tables.include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + deleted_tables << table end table_rows.each do |fixture_set_name, rows| @@ -534,12 +539,10 @@ module ActiveRecord conn.insert_fixture(row, fixture_set_name) end end - end - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - fixture_sets.each do |fs| - connection.reset_pk_sequence!(fs.table_name) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + conn.reset_pk_sequence!(fs.table_name) end end end @@ -612,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') @@ -641,6 +643,11 @@ module ActiveRecord row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) end + # Resolve enums + model_class.defined_enums.each do |name, values| + row[name] = values.fetch(row[name], row[name]) + end + # If STI is used, find the correct subclass for association reflection reflection_class = if row.include?(inheritance_column_name) @@ -661,7 +668,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.columns_hash[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 @@ -691,7 +698,7 @@ module ActiveRecord end def primary_key_type - @association.klass.column_types[@association.klass.primary_key].type + @association.klass.type_for_attribute(@association.klass.primary_key).type end end @@ -703,6 +710,10 @@ module ActiveRecord def lhs_key @association.through_reflection.foreign_key end + + def join_table + @association.through_reflection.table_name + end end private @@ -711,7 +722,7 @@ module ActiveRecord end def primary_key_type - @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type + @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type end def add_join_records(rows, row, association) @@ -828,13 +839,15 @@ module ActiveRecord class_attribute :fixture_path, :instance_writer => false class_attribute :fixture_table_names class_attribute :fixture_class_names + class_attribute :use_transactional_tests class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures class_attribute :config + singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead' + self.fixture_table_names = [] - self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.config = ActiveRecord::Base @@ -842,6 +855,16 @@ module ActiveRecord self.fixture_class_names = Hash.new do |h, fixture_set_name| h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) end + + silence_warnings do + define_singleton_method :use_transactional_tests do + if use_transactional_fixtures.nil? + true + else + use_transactional_fixtures + end + end + end end module ClassMethods @@ -882,7 +905,7 @@ module ActiveRecord @fixture_cache[fs_name] ||= {} instances = fixture_names.map do |f_name| - f_name = f_name.to_s + f_name = f_name.to_s if f_name.is_a?(Symbol) @fixture_cache[fs_name].delete(f_name) if force_reload if @loaded_fixtures[fs_name][f_name] @@ -912,13 +935,13 @@ module ActiveRecord end def run_in_transaction? - use_transactional_fixtures && + use_transactional_tests && !self.class.uses_transaction?(method_name) end def setup_fixtures(config = ActiveRecord::Base) - if pre_loaded_fixtures && !use_transactional_fixtures - raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' + if pre_loaded_fixtures && !use_transactional_tests + raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests' end @fixture_cache = {} diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fd1e22349b..e613d157aa 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -192,20 +192,22 @@ module ActiveRecord # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil def subclass_from_attributes?(attrs) - columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) end def subclass_from_attributes(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass_name.present? && subclass_name != self.name - subclass = subclass_name.safe_constantize + if subclass_name.present? + subclass = find_sti_class(subclass_name) - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") - end + if subclass.name != self.name + unless descendants.include?(subclass) + raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}") + end - subclass + subclass + end end end end diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb new file mode 100644 index 0000000000..89dee58423 --- /dev/null +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module LegacyYamlAdapter + def self.convert(klass, coder) + return coder unless coder.is_a?(Psych::Coder) + + case coder["active_record_yaml_version"] + when 1 then coder + else + if coder["attributes"].is_a?(AttributeSet) + Rails420.convert(klass, coder) + else + Rails41.convert(klass, coder) + end + end + end + + module Rails420 + def self.convert(klass, coder) + attribute_set = coder["attributes"] + + klass.attribute_names.each do |attr_name| + attribute = attribute_set[attr_name] + if attribute.type.is_a?(Delegator) + type_from_klass = klass.type_for_attribute(attr_name) + attribute_set[attr_name] = attribute.with_type(type_from_klass) + end + end + + coder + end + end + + module Rails41 + def self.convert(klass, coder) + attributes = klass.attributes_builder + .build_from_database(coder["attributes"]) + new_record = coder["attributes"][klass.primary_key].blank? + + { + "attributes" => attributes, + "new_record" => new_record, + } + end + end + end +end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index b1fbd38622..8a3c27e6da 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -7,6 +7,7 @@ en: # Default error messages errors: messages: + required: "must exist" taken: "has already been taken" # Active Record models configuration diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9f053453bd..a09437b4b0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,6 +66,15 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + if locking_enabled? + # We always want to persist the locking version, even if we don't detect + # a change from the default, since the database might have no default + attribute_names |= [self.class.locking_column] + end + super + end + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -84,7 +93,7 @@ module ActiveRecord self.class.primary_key => id, lock_col => previous_lock_value, ).update_all( - attribute_names.map do |name| + attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] end.to_h ) @@ -116,12 +125,8 @@ module ActiveRecord relation = super if locking_enabled? - column_name = self.class.locking_column - column = self.class.columns_hash[column_name] - substitute = self.class.connection.substitute_at(column) - - relation = relation.where(self.class.arel_table[column_name].eq(substitute)) - relation.bind_values << [column, self[column_name].to_i] + locking_column = self.class.locking_column + relation = relation.where(locking_column => _read_attribute(locking_column)) end relation @@ -139,7 +144,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - clear_caches_calculated_from_columns + reload_schema_from_cache @locking_column = value.to_s end @@ -179,17 +184,12 @@ module ActiveRecord end end - class LockingType < SimpleDelegator # :nodoc: - def type_cast_from_database(value) + class LockingType < DelegateClass(Type::Value) # :nodoc: + def deserialize(value) # `nil` *should* be changed to 0 super.to_i end - def changed?(old_value, *) - # Ensure we save if the default was `nil` - super || old_value == 0 - end - def init_with(coder) __setobj__(coder['subtype']) end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index ff7102d35b..3d95c54ef3 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index eb64d197f0..af816a278e 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -20,24 +20,21 @@ module ActiveRecord @odd = false end - def render_bind(column, value) - if column - if column.binary? - # This specifically deals with the PG adapter that casts bytea columns into a Hash. - value = value[:value] if value.is_a?(Hash) - value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" - end - - [column.name, value] + def render_bind(attribute) + value = if attribute.type.binary? && attribute.value + "<#{attribute.value.bytesize} bytes of binary data>" else - [nil, value] + attribute.value_for_database end + + [attribute.name, value] end def sql(event) - self.class.runtime += event.duration return unless logger.debug? + self.class.runtime += event.duration + payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) @@ -47,9 +44,7 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |col,v| - render_bind(col, v) - }.inspect + binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end if odd? diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 46f4794010..192a456846 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -168,7 +168,7 @@ module ActiveRecord # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration # def change - # add_column :tablenames, :field, :string + # add_column :tablenames, :fieldname, :string # end # end # @@ -275,21 +275,6 @@ module ActiveRecord # The phrase "Updating salaries..." would then be printed, along with the # benchmark for the block when the block completes. # - # == About the schema_migrations table - # - # Rails versions 2.0 and prior used to create a table called - # <tt>schema_info</tt> when using migrations. This table contained the - # version of the schema as of the last applied migration. - # - # Starting with Rails 2.1, the <tt>schema_info</tt> table is - # (automatically) replaced by the <tt>schema_migrations</tt> table, which - # contains the version numbers of all the migrations applied. - # - # As a result, it is now possible to add migration files that are numbered - # lower than the current schema version: when migrating up, those - # never-applied "interleaved" migrations will be automatically applied, and - # when migrating down, never-applied "interleaved" migrations will be skipped. - # # == Timestamped Migrations # # By default, Rails generates migrations that look like: @@ -307,9 +292,8 @@ module ActiveRecord # # == Reversible Migrations # - # Starting with Rails 3.1, you will be able to define reversible migrations. # Reversible migrations are migrations that know how to go +down+ for you. - # You simply supply the +up+ logic, and the Migration system will figure out + # You simply supply the +up+ logic, and the Migration system figures out # how to execute the down commands for you. # # To define a reversible migration, define the +change+ method in your @@ -395,7 +379,7 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? - # Roundrip to Rake to allow plugins to hook into database initialization. + # Roundtrip to Rake to allow plugins to hook into database initialization. FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 36256415df..ee4545ed71 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -184,6 +184,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 641512d323..5a6f42ba09 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -111,17 +111,6 @@ module ActiveRecord # class Mouse < ActiveRecord::Base # self.table_name = "mice" # end - # - # Alternatively, you can override the table_name method to define your - # own computation. (Possibly using <tt>super</tt> to manipulate the default - # table name.) Example: - # - # class Post < ActiveRecord::Base - # def self.table_name - # "special_" + super - # end - # end - # Post.table_name # => "special_posts" def table_name reset_table_name unless defined?(@table_name) @table_name @@ -132,9 +121,6 @@ module ActiveRecord # class Project < ActiveRecord::Base # self.table_name = "project" # end - # - # You can also just define your own <tt>self.table_name</tt> method; see - # the documentation for ActiveRecord::Base#table_name. def table_name=(value) value = value && value.to_s @@ -231,28 +217,37 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key) + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) end - def column_types # :nodoc: - @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| - h.default = Type::Value.new - end + def columns_hash # :nodoc: + load_schema + @columns_hash + end + + def columns + load_schema + @columns ||= columns_hash.values + end + + def attribute_types # :nodoc: + load_schema + @attribute_types ||= Hash.new(Type::Value.new) end def type_for_attribute(attr_name) # :nodoc: - column_types[attr_name] + attribute_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults + load_schema _default_attributes.to_hash end def _default_attributes # :nodoc: - @default_attributes ||= attributes_builder.build_from_database( - raw_default_values) + @default_attributes ||= AttributeSet.new({}) end # Returns an array of column names as strings. @@ -295,19 +290,50 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? + connection.schema_cache.clear_table_cache!(table_name) - @arel_engine = nil - @arel_table = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @default_attributes = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + reload_schema_from_cache end private + def schema_loaded? + defined?(@columns_hash) && @columns_hash + end + + def load_schema + unless schema_loaded? + load_schema! + end + end + + 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), + default: column.default, + user_provided_default: false + ) + end + end + + def reload_schema_from_cache + @arel_engine = nil + @arel_table = nil + @column_names = nil + @attribute_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @attributes_builder = nil + @columns = nil + @columns_hash = nil + @attribute_names = nil + end + # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore @@ -332,8 +358,26 @@ module ActiveRecord end end - def raw_default_values - columns_hash.transform_values(&:default) + 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 diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 846e1162a9..c942d0e265 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -147,8 +147,8 @@ module ActiveRecord # has_many :posts # accepts_nested_attributes_for :posts, reject_if: :reject_posts # - # def reject_posts(attributed) - # attributed['title'].blank? + # def reject_posts(attributes) + # attributes['title'].blank? # end # end # @@ -166,6 +166,11 @@ module ActiveRecord # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' # member.posts.second.title # => '[UPDATED] other post' # + # However, the above applies if the parent model is being updated as well. + # For example, If you wanted to create a +member+ named _joe_ and wanted to + # update the +posts+ at the same time, that would give an + # ActiveRecord::RecordNotFound error. + # # By default the associated records are protected from being destroyed. If # you want to destroy any of the associated records through the attributes # hash, you have to enable it first using the <tt>:allow_destroy</tt> @@ -208,7 +213,7 @@ module ActiveRecord # # Passing attributes for an associated collection in the form of a hash # of hashes can be used with hashes generated from HTTP/HTML parameters, - # where there maybe no natural way to submit an array of hashes. + # where there may be no natural way to submit an array of hashes. # # === Saving # @@ -312,7 +317,7 @@ module ActiveRecord attr_names.each do |association_name| if reflection = _reflect_on_association(association_name) reflection.autosave = true - add_autosave_association_callbacks(reflection) + define_autosave_validation_callbacks(reflection) nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options @@ -521,7 +526,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - Type::Boolean.new.type_cast_from_user(hash['_destroy']) + Type::Boolean.new.cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index b406da14dc..74894d0c37 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -14,7 +14,7 @@ module ActiveRecord 0 end - def update_all(_updates, _conditions = nil, _options = {}) + def update_all(_updates) 0 end @@ -30,10 +30,18 @@ module ActiveRecord true end + def none? + true + end + def any? false end + def one? + false + end + def many? false end @@ -72,8 +80,12 @@ module ActiveRecord end end - def exists?(_id = false) + def exists?(_conditions = :none) false end + + def or(other) + other.spawn + end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index cf6673db2e..0a6e4ac0bd 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -96,7 +96,8 @@ module ActiveRecord # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - !(new_record? || destroyed?) + sync_with_transaction_state + !(@new_record || @destroyed) end # Saves the model. @@ -147,7 +148,7 @@ module ActiveRecord # Attributes marked as readonly are silently ignored if the record is # being updated. def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) + create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -178,6 +179,7 @@ module ActiveRecord def destroy raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? destroy_associations + self.class.connection.add_transaction_record(self) destroy_row if persisted? @destroyed = true freeze @@ -191,7 +193,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed, self) + destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self)) end # Returns an instance of the specified +klass+ with the attributes of the @@ -203,11 +205,13 @@ module ActiveRecord # instance using the companies/company partial instead of clients/client. # # Note: The new instance will share a link to the same attributes as the original class. - # So any change to the attributes in either instance will affect the other. + # Therefore the sti column value will still be the same. + # Any change to the attributes on either instance will affect both instances. + # If you want to change the sti column as well, use +becomes!+ instead. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@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) @@ -245,8 +249,8 @@ module ActiveRecord def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) - send("#{name}=", value) - save(validate: false) + public_send("#{name}=", value) + save(validate: false) if changed? end # Updates the attributes of the model from the passed-in hash and saves the @@ -352,7 +356,7 @@ module ActiveRecord # method toggles directly the underlying value without calling any setter. # Returns +self+. def toggle(attribute) - self[attribute] = !send("#{attribute}?") + self[attribute] = !public_send("#{attribute}?") self end @@ -377,7 +381,7 @@ module ActiveRecord # # => #<Account id: 1, email: 'account@example.com'> # # Attributes are reloaded from the database, and caches busted, in - # particular the associations cache. + # particular the associations cache and the QueryCache. # # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> # is raised. Otherwise, in addition to the in-place modification the method @@ -413,8 +417,7 @@ module ActiveRecord # end # def reload(options = nil) - clear_aggregation_cache - clear_association_cache + self.class.connection.clear_query_cache fresh_object = if options && options[:lock] @@ -428,14 +431,17 @@ module ActiveRecord self end - # Saves the record with the updated_at/on attributes set to the current time. + # Saves the record with the updated_at/on attributes set to the current time + # or the time specified. # Please note that no validation is performed and only the +after_touch+, # +after_commit+ and +after_rollback+ callbacks are executed. # + # This method can be passed attribute names and an optional time argument. # If attribute names are passed, they are updated along with updated_at/on - # attributes. + # attributes. If no time argument is passed, the current time is used as default. # - # product.touch # updates updated_at/on + # product.touch # updates updated_at/on with current time + # product.touch(time: Time.new(2015, 2, 16, 0, 0, 0)) # updates updated_at/on with specified time # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes # @@ -459,26 +465,38 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names) + def touch(*names, time: nil) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model attributes.concat(names) unless attributes.empty? - current_time = current_time_from_proper_timezone changes = {} attributes.each do |column| column = column.to_s - changes[column] = write_attribute(column, current_time) + changes[column] = write_attribute(column, time) end - changes[self.class.locking_column] = increment_lock if locking_enabled? - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key - self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 + scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) + + if locking_enabled? + locking_column = self.class.locking_column + scope = scope.where(locking_column => _read_attribute(locking_column)) + changes[locking_column] = increment_lock + end + + result = scope.update_all(changes) == 1 + + if !result && locking_enabled? + raise ActiveRecord::StaleObjectError.new(self, "touch") + end + + result else true end @@ -495,15 +513,7 @@ module ActiveRecord end def relation_for_destroy - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation + self.class.unscoped.where(self.class.primary_key => id) end def create_or_update(*args) diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 91c9a0db99..4e597590e9 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord 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 :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + 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 delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index f1bdbc845c..da6b8447d3 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -93,6 +93,7 @@ module ActiveRecord cache = Marshal.load File.binread filename if cache.version == ActiveRecord::Migrator.current_version self.connection.schema_cache = cache + self.connection_pool.schema_cache = cache.dup else warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end @@ -102,6 +103,14 @@ module ActiveRecord end end + initializer "active_record.warn_on_records_fetched_greater_than" do + if config.active_record.warn_on_records_fetched_greater_than + ActiveSupport.on_load(:active_record) do + require 'active_record/relation/record_fetch_warning' + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| @@ -147,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/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index af4840476c..8727e46cb3 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -19,7 +19,7 @@ module ActiveRecord end def cleanup_view_runtime - if ActiveRecord::Base.connected? + if logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 04c2be045d..d168786e71 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -240,7 +240,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => [:load_config] do + task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -269,9 +269,9 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' + desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -286,8 +286,8 @@ db_namespace = namespace :db do end desc "Recreate the databases from the structure.sql file" - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) + task :load => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end task :load_if_sql => ['db:create', :environment] do diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index dab5a502a5..5360db6a19 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -69,9 +69,11 @@ module ActiveRecord def reflections ref = {} _reflections.each do |name, reflection| - parent_name, parent_reflection = reflection.parent_reflection - if parent_name - ref[parent_name] = parent_reflection + parent_reflection = reflection.parent_reflection + + if parent_reflection + parent_name = parent_reflection.name + ref[parent_name.to_s] = parent_reflection else ref[name] = reflection end @@ -166,8 +168,8 @@ module ActiveRecord # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection + # AggregateReflection # AssociationReflection - # AggregateReflection # HasManyReflection # HasOneReflection # BelongsToReflection @@ -196,7 +198,7 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record - @klass = options[:class] + @klass = options[:anonymous_class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end @@ -204,7 +206,7 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave - _, parent_reflection = self.parent_reflection + parent_reflection = self.parent_reflection if parent_reflection parent_reflection.autosave = autosave end @@ -272,7 +274,7 @@ module ActiveRecord end attr_reader :type, :foreign_type - attr_accessor :parent_reflection # [:name, Reflection] + attr_accessor :parent_reflection # Reflection def initialize(name, scope, options, active_record) super @@ -370,6 +372,12 @@ module ActiveRecord [self] end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @association_scope_cache.clear + end + def nested? false end @@ -629,7 +637,7 @@ module ActiveRecord def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end @@ -694,7 +702,7 @@ module ActiveRecord def chain @chain ||= begin a = source_reflection.chain - b = through_reflection.chain + b = through_reflection.chain.map(&:dup) if options[:source_type] b[0] = PolymorphicReflection.new(b[0], self) @@ -706,6 +714,15 @@ module ActiveRecord end end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @chain = nil + delegate_reflection.clear_association_scope_cache + source_reflection.clear_association_scope_cache + through_reflection.clear_association_scope_cache + end + # Consider the following example: # # class Person diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ab3debc03b..7d37313058 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- -require 'arel/collectors/bind' +require "arel/collectors/bind" module ActiveRecord # = Active Record Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :where, :having, :bind, :references, + :order, :joins, :references, :extending, :unscope] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :distinct, :create_with, :uniq] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, + :reverse_order, :distinct, :create_with] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] - VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation @@ -33,7 +34,6 @@ module ActiveRecord # This method is a hot spot, so for now, use Hash[] to dup the hash. # https://bugs.ruby-lang.org/issues/7166 @values = Hash[@values] - @values[:bind] = @values[:bind].dup if @values.key? :bind reset end @@ -81,7 +81,7 @@ module ActiveRecord end relation = scope.where(@klass.primary_key => (id_was || id)) - bvs = binds + relation.bind_values + bvs = binds + relation.bound_attributes um = relation .arel .compile_update(substitutes, @klass.primary_key) @@ -95,11 +95,11 @@ module ActiveRecord def substitute_values(values) # :nodoc: binds = values.map do |arel_attr, value| - [@klass.columns_hash[arel_attr.name], value] + QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) end - substitutes = values.each_with_index.map do |(arel_attr, _), i| - [arel_attr, @klass.connection.substitute_at(binds[i][0])] + substitutes = values.map do |(arel_attr, _)| + [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] end [substitutes, binds] @@ -205,7 +205,9 @@ module ActiveRecord # constraint an exception may be raised, just retry: # # begin - # CreditAccount.find_or_create_by(user_id: user.id) + # CreditAccount.transaction(requires_new: true) do + # CreditAccount.find_or_create_by(user_id: user.id) + # end # rescue ActiveRecord::RecordNotUnique # retry # end @@ -271,6 +273,15 @@ module ActiveRecord end end + # Returns true if there are no records. + def none? + if block_given? + to_a.none? { |*block_args| yield(*block_args) } + else + empty? + end + end + # Returns true if there are any records. def any? if block_given? @@ -280,6 +291,15 @@ module ActiveRecord end 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 + end + # Returns true if there is more than one record. def many? if block_given? @@ -342,8 +362,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - bvs = arel.bind_values + bind_values - @klass.connection.update stmt, 'SQL', bvs + @klass.connection.update stmt, 'SQL', bound_attributes end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -467,8 +486,10 @@ module ActiveRecord invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| if MULTI_VALUE_METHODS.include?(method) send("#{method}_values").any? - else + elsif SINGLE_VALUE_METHODS.include?(method) send("#{method}_value") + elsif CLAUSE_METHODS.include?(method) + send("#{method}_clause").any? end } if invalid_methods.any? @@ -487,7 +508,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - affected = @klass.connection.delete(stmt, 'SQL', bind_values) + affected = @klass.connection.delete(stmt, 'SQL', bound_attributes) reset affected @@ -557,10 +578,10 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - arel = relation.arel - binds = (arel.bind_values + relation.bind_values).dup - binds.map! { |bv| connection.quote(*bv.reverse) } - collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + binds = relation.bound_attributes + binds = connection.prepare_binds_for_database(binds) + binds.map! { |value| connection.quote(value) } + collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new) collect.substitute_binds(binds).join end end @@ -570,22 +591,7 @@ module ActiveRecord # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} def where_values_hash(relation_table_name = table_name) - equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| - node.left.relation.name == relation_table_name - } - - binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] - - Hash[equalities.map { |where| - name = where.left.name - [name, binds.fetch(name.to_s) { - case where.right - when Array then where.right.map(&:val) - when Arel::Nodes::Casted, Arel::Nodes::Quoted - where.right.val - end - }] - }] + where_clause.to_h(relation_table_name) end def scope_for_create @@ -612,6 +618,7 @@ module ActiveRecord def uniq_value distinct_value end + deprecate uniq_value: :distinct_value # Compares two relations for equality. def ==(other) @@ -648,7 +655,7 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4f0502ae75..e07580a563 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,14 +27,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <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 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 +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(start: 2000, batch_size: 2000) do |person| + # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -45,19 +46,27 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(options = {}) + def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end if block_given? - find_in_batches(options) do |records| + find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for :find_each, options do - options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + relation = self + apply_limits(relation, begin_at, end_at).size end end end - # Yields each batch of records that was found by the find +options+ as + # Yields each batch of records that was found by the find options as # an array. # # Person.where("age > 21").find_in_batches do |group| @@ -77,14 +86,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <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 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 +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -95,16 +105,19 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(options = {}) - options.assert_valid_keys(:start, :batch_size) + def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end relation = self - start = options[:start] - batch_size = options[:batch_size] || 1000 - unless block_given? - return to_enum(:find_in_batches, options) do - total = start ? where(table[primary_key].gteq(start)).size : size + return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + total = apply_limits(relation, begin_at, end_at).size (total - 1).div(batch_size) + 1 end end @@ -114,7 +127,8 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a + relation = apply_limits(relation, begin_at, end_at) + records = relation.to_a while records.any? records_size = records.size @@ -131,6 +145,12 @@ module ActiveRecord private + def apply_limits(relation, begin_at, end_at) + relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at + relation = relation.where(table[primary_key].lteq(end_at)) if end_at + relation + end + def batch_order "#{quoted_table_name}.#{quoted_primary_key} ASC" end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 1d4cb1a83b..7a28a98721 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -130,9 +130,9 @@ module ActiveRecord # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # - # Person.pluck(:id) - # # SELECT people.id FROM people - # # => [1, 2, 3] + # Person.pluck(:name) + # # SELECT people.name FROM people + # # => ['David', 'Jeremy', 'Jose'] # # Person.pluck(:id, :name) # # SELECT people.id, people.name FROM people @@ -150,6 +150,8 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # + # See also +ids+. + # def pluck(*column_names) column_names.map! do |column_name| if column_name.is_a?(Symbol) && attribute_alias?(column_name) @@ -159,6 +161,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 @@ -166,8 +172,8 @@ module ActiveRecord relation.select_values = column_names.map { |cn| columns_hash.key?(cn) ? arel_table[cn] : cn } - result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values) - result.cast_values(klass.column_types) + result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result.cast_values(klass.attribute_types) end end @@ -182,7 +188,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && column_name && column_name != :all) end def perform_calculation(operation, column_name) @@ -222,19 +228,16 @@ module ActiveRecord end def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # Postgresql doesn't like ORDER BY when there are no GROUP BY + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY relation = unscope(:order) column_alias = column_name - bind_values = nil - if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) - bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -245,10 +248,9 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel - bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, bind_values) + result = @klass.connection.select_all(query_builder, nil, bound_attributes) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do @@ -290,7 +292,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_values.empty? + select_values += select_values unless having_clause.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| if field.respond_to?(:as) @@ -304,11 +306,11 @@ module ActiveRecord relation.group_values = group relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values) + calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } - key_records = association.klass.base_class.find(key_ids) + key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end @@ -357,9 +359,9 @@ module ActiveRecord def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type.type_cast_from_database(value || 0) + when 'sum' then type.deserialize(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type.type_cast_from_database(value) + else type.deserialize(value) end end @@ -378,11 +380,9 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - arel = relation.arel - subquery = arel.as(subquery_alias) + subquery = relation.arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine - sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d4a8823cfe..86f2c30168 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -39,7 +39,7 @@ module ActiveRecord BLACKLISTED_ARRAY_METHODS = [ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact, :select! + :keep_if, :pop, :shift, :delete_at, :select! ].to_set # :nodoc: delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 088bc203b7..6020aa238f 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/core_ext/string/filters' module ActiveRecord @@ -6,7 +5,7 @@ module ActiveRecord ONE_AS_ONE = '1 AS one' # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # # Person.find(1) # returns the object for ID = 1 @@ -17,8 +16,6 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. - # # NOTE: The returned records may not be in the same order as the ids you # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> # option if you want the results are sorted. @@ -49,7 +46,7 @@ module ActiveRecord # # returns the first item or returns a new instance (requires you call .save to persist against the database). # # Person.where(name: 'Spartacus', rating: 4).first_or_create - # # returns the first item or creates it and returns it, available since Rails 3.2.1. + # # returns the first item or creates it and returns it. # # ==== Alternatives for +find+ # @@ -60,10 +57,10 @@ module ActiveRecord # # returns a chainable list of instances with only the mentioned fields. # # Person.where(name: 'Spartacus', rating: 4).ids - # # returns an Array of ids, available since Rails 3.2.1. + # # returns an Array of ids. # # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2) - # # returns an Array of the required fields, available since Rails 3.1. + # # returns an Array of the required fields. def find(*args) if block_given? to_a.find(*args) { |*block_args| yield(*block_args) } @@ -80,16 +77,16 @@ module ActiveRecord # # Post.find_by name: 'Spartacus', rating: 4 # Post.find_by "published_at < ?", 2.weeks.ago - def find_by(*args) - where(*args).take + def find_by(arg, *args) + where(arg, *args).take rescue RangeError nil end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. - def find_by!(*args) - where(*args).take! + def find_by!(arg, *args) + where(arg, *args).take! rescue RangeError raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" end @@ -114,23 +111,11 @@ module ActiveRecord # Find the first record (or first N records if a parameter is supplied). # If no order is defined it will order by primary key. # - # Person.first # returns the first object fetched by SELECT * FROM people + # Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1 # Person.where(["user_name = ?", user_name]).first # Person.where(["user_name = :u", { u: user_name }]).first # Person.order("created_on DESC").offset(5).first - # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 - # - # ==== Rails 3 - # - # Person.first # SELECT "people".* FROM "people" LIMIT 1 - # - # NOTE: Rails 3 may not order this query by the primary key and the order - # will depend on the database implementation. In order to ensure that behavior, - # use <tt>User.order(:id).first</tt> instead. - # - # ==== Rails 4 - # - # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1 + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3 # def first(limit = nil) if limit @@ -307,11 +292,11 @@ module ActiveRecord relation = relation.where(conditions) else unless conditions == :none - relation = where(primary_key => conditions) + relation = relation.where(primary_key => conditions) end end - connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false + connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false end # This method is called whenever no records are found with either a single @@ -365,7 +350,7 @@ module ActiveRecord [] else arel = relation.arel - rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) + rows = connection.select_all(arel, 'SQL', relation.bound_attributes) join_dependency.instantiate(rows, aliases) end end @@ -379,7 +364,7 @@ module ActiveRecord def construct_relation_for_association_calculations from = arel.froms.first if Arel::Table === from - apply_join_dependency(self, construct_join_dependency) + apply_join_dependency(self, construct_join_dependency(joins_values)) else # FIXME: as far as I can tell, `from` will always be an Arel::Table. # There are no tests that test this branch, but presumably it's @@ -410,7 +395,7 @@ module ActiveRecord relation = relation.except(:select).select(values).distinct! arel = relation.arel - id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) + id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes) id_rows.map {|row| row[primary_key]} end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb new file mode 100644 index 0000000000..a93952fa30 --- /dev/null +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class Relation + class FromClause + attr_reader :value, :name + + def initialize(value, name) + @value = value + @name = name + end + + def binds + if value.is_a?(Relation) + value.bound_attributes + else + [] + end + end + + def merge(other) + self + end + + def empty? + value.nil? + end + + def self.empty + new(nil, nil) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index afb0b208c3..dd8f0aa298 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/hash/keys' -require "set" module ActiveRecord class Relation @@ -49,9 +48,9 @@ module ActiveRecord @other = other end - NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + - Relation::MULTI_VALUE_METHODS - - [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + NORMAL_VALUES = Relation::VALUE_METHODS - + Relation::CLAUSE_METHODS - + [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -75,6 +74,8 @@ module ActiveRecord merge_multi_values merge_single_values + merge_clauses + merge_preloads merge_joins relation @@ -82,6 +83,27 @@ module ActiveRecord private + def merge_preloads + 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? + else + reflection = relation.klass.reflect_on_all_associations.find do |r| + r.class_name == other.klass.name + end || return + + unless other.preload_values.empty? + relation.preload! reflection.name => other.preload_values + end + + unless other.includes_values.empty? + relation.includes! reflection.name => other.includes_values + end + end + end + def merge_joins return if other.joins_values.blank? @@ -107,20 +129,6 @@ module ActiveRecord end def merge_multi_values - lhs_wheres = relation.where_values - rhs_wheres = other.where_values - - lhs_binds = relation.bind_values - rhs_binds = other.bind_values - - removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - - where_values = kept + rhs_wheres - bind_values = filter_binds(lhs_binds, removed) + rhs_binds - - relation.where_values = where_values - relation.bind_values = bind_values - if other.reordering_value # override any order specified in the original relation relation.reorder! other.order_values @@ -133,36 +141,18 @@ module ActiveRecord end def merge_single_values - relation.from_value = other.from_value unless relation.from_value - relation.lock_value = other.lock_value unless relation.lock_value + relation.lock_value ||= other.lock_value unless other.create_with_value.blank? relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) end end - def filter_binds(lhs_binds, removed_wheres) - return lhs_binds if removed_wheres.empty? - - set = Set.new removed_wheres.map { |x| x.left.name.to_s } - lhs_binds.dup.delete_if { |col,_| set.include? col.name } - end - - # Remove equalities from the existing relation with a LHS which is - # present in the relation being merged in. - # returns [things_to_remove, things_to_keep] - def partition_overwrites(lhs_wheres, rhs_wheres) - if lhs_wheres.empty? || rhs_wheres.empty? - return [[], lhs_wheres] - end - - nodes = rhs_wheres.find_all do |w| - w.respond_to?(:operator) && w.operator == :== - end - seen = Set.new(nodes) { |node| node.left } - - lhs_wheres.partition do |w| - w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) + def merge_clauses + CLAUSE_METHODS.each do |name| + clause = relation.send("#{name}_clause") + other_clause = other.send("#{name}_clause") + relation.send("#{name}_clause=", clause.merge(other_clause)) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 567efce8ae..43e9afe853 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -28,6 +28,11 @@ module ActiveRecord expand_from_hash(attributes) end + def create_binds(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + create_binds_for_hash(attributes) + end + def expand(column, value) # Find the foreign key when using queries such as: # Post.where(author: author) @@ -80,16 +85,43 @@ module ActiveRecord attributes.flat_map do |key, value| if value.is_a?(Hash) - builder = self.class.new(table.associated_table(key)) - builder.expand_from_hash(value) + associated_predicate_builder(key).expand_from_hash(value) else expand(key, value) end end end + + def create_binds_for_hash(attributes) + result = attributes.dup + binds = [] + + attributes.each do |column_name, value| + case value + when Hash + attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) + result[column_name] = attrs + binds += bvs + when Relation + binds += value.bound_attributes + else + if can_be_bound?(column_name, value) + result[column_name] = Arel::Nodes::BindParam.new + binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end + end + end + + [result, binds] + end + private + def associated_predicate_builder(association_name) + self.class.new(table.associated_table(association_name)) + end + def convert_dot_notation_to_hash(attributes) dot_notation = attributes.keys.select { |s| s.include?(".") } @@ -107,5 +139,11 @@ module ActiveRecord def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end + + def can_be_bound?(column_name, value) + !value.nil? && + handler_for(value).is_a?(BasicObjectHandler) && + !table.associated_with?(column_name) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index aabcf20c1d..159889d3b8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -31,7 +31,14 @@ module ActiveRecord end def ids - value + case value + when Relation + value.select(primary_key) + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end end def base_class @@ -42,6 +49,10 @@ module ActiveRecord private + def primary_key + associated_table.association_primary_key(base_class) + end + def polymorphic_base_class_from_value case value when Relation @@ -53,6 +64,15 @@ module ActiveRecord value.class.base_class end end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb new file mode 100644 index 0000000000..e69319b4de --- /dev/null +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -0,0 +1,19 @@ +require 'active_record/attribute' + +module ActiveRecord + class Relation + class QueryAttribute < Attribute + def type_cast(value) + value + end + + def value_for_database + @value_for_database ||= super + end + + def with_cast_value(value) + QueryAttribute.new(name, value, type) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index f054e17017..f85dc35e89 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,6 +1,9 @@ -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/string/filters' +require "active_record/relation/from_clause" +require "active_record/relation/query_attribute" +require "active_record/relation/where_clause" +require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' +require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods @@ -39,23 +42,10 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) - where_value = @scope.send(:build_where, opts, rest).map do |rel| - case rel - when NilClass - raise ArgumentError, 'Invalid argument for .where.not(), got nil.' - when Arel::Nodes::In - Arel::Nodes::NotIn.new(rel.left, rel.right) - when Arel::Nodes::Equality - Arel::Nodes::NotEqual.new(rel.left, rel.right) - when String - Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel)) - else - Arel::Nodes::Not.new(rel) - end - end + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts - @scope.where_values += where_value + @scope.where_clause += where_clause.invert @scope end end @@ -90,6 +80,23 @@ module ActiveRecord CODE end + Relation::CLAUSE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_clause # def where_clause + @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause + end # end + # + def #{name}_clause=(value) # def where_clause=(value) + assert_mutability! # assert_mutability! + @values[:#{name}] = value # @values[:where] = value + end # end + CODE + end + + def bound_attributes + from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -392,9 +399,8 @@ module ActiveRecord raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." end - Array(target_value).each do |val| - where_unscoping(val) - end + target_values = Array(target_value).map(&:to_s) + self.where_clause = where_clause.except(*target_values) end else raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." @@ -425,15 +431,6 @@ module ActiveRecord self end - def bind(value) # :nodoc: - spawn.bind!(value) - end - - def bind!(value) # :nodoc: - self.bind_values += [value] - self - end - # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -569,7 +566,7 @@ module ActiveRecord references!(PredicateBuilder.references(opts)) end - self.where_values += build_where(opts, rest) + self.where_clause += where_clause_factory.build(opts, rest) self end @@ -585,6 +582,37 @@ module ActiveRecord unscope(where: conditions.keys).where(conditions) end + # Returns a new relation, which is the logical union of this relation and the one passed as an + # argument. + # + # The two relations must be structurally compatible: they must be scoping the same model, and + # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is + # present). Neither relation may have a +limit+, +offset+, or +distinct+ set. + # + # Post.where("id = 1").or(Post.where("id = 2")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # + def or(other) + spawn.or!(other) + end + + def or!(other) # :nodoc: + unless structurally_compatible_for_or?(other) + raise ArgumentError, 'Relation passed to #or must be structurally compatible' + end + + self.where_clause = self.where_clause.or(other.where_clause) + self.having_clause = self.having_clause.or(other.having_clause) + + self + end + + private def structurally_compatible_for_or?(other) # :nodoc: + Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && + (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && + (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -596,7 +624,7 @@ module ActiveRecord def having!(opts, *rest) # :nodoc: references!(PredicateBuilder.references(opts)) if Hash === opts - self.having_values += build_where(opts, rest) + self.having_clause += having_clause_factory.build(opts, rest) self end @@ -744,10 +772,7 @@ module ActiveRecord end def from!(value, subquery_name = nil) # :nodoc: - self.from_value = [value, subquery_name] - if value.is_a? Relation - self.bind_values = value.arel.bind_values + value.bind_values + bind_values - end + self.from_clause = Relation::FromClause.new(value, subquery_name) self end @@ -765,6 +790,7 @@ module ActiveRecord spawn.distinct!(value) end alias uniq distinct + deprecate uniq: :distinct # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: @@ -772,6 +798,7 @@ module ActiveRecord self end alias uniq! distinct! + deprecate uniq!: :distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -858,21 +885,18 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds - - arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? - + arel.where(where_clause.ast) unless where_clause.empty? + arel.having(having_clause.ast) unless having_clause.empty? arel.take(connection.sanitize_limit(limit_value)) if limit_value arel.skip(offset_value.to_i) if offset_value - - arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty? + arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) - build_select(arel, select_values.uniq) + build_select(arel) arel.distinct(distinct_value) - arel.from(build_from) if from_value + arel.from(build_from) unless from_clause.empty? arel.lock(lock_value) if lock_value arel @@ -883,114 +907,24 @@ module ActiveRecord raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) - unscope_code = "#{scope}_value#{'s' unless single_val_method}=" + clause_method = Relation::CLAUSE_METHODS.include?(scope) + multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope) + if clause_method + unscope_code = "#{scope}_clause=" + else + unscope_code = "#{scope}_value#{'s' if multi_val_method}=" + end case scope when :order result = [] - when :where - self.bind_values = [] else - result = [] unless single_val_method + result = [] if multi_val_method end self.send(unscope_code, result) end - def where_unscoping(target_value) - target_value = target_value.to_s - - where_values.reject! do |rel| - case rel - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual - subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name.to_s == target_value - end - end - - bind_values.reject! { |col,_| col.name == target_value } - end - - def custom_join_ast(table, joins) - joins = joins.reject(&:blank?) - - return [] if joins.empty? - - joins.map! do |join| - case join - when Array - join = Arel.sql(join.join(' ')) if array_of_strings?(join) - when String - join = Arel.sql(join) - end - table.create_string_join(join) - end - end - - def collapse_wheres(arel, wheres) - predicates = wheres.map do |where| - next where if ::Arel::Nodes::Equality === where - where = Arel.sql(where) if String === where - Arel::Nodes::Grouping.new(where) - end - - arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? - end - - def build_where(opts, other = []) - case opts - when String, Array - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] - when Hash - opts = predicate_builder.resolve_column_aliases(opts) - - tmp_opts, bind_values = create_binds(opts) - self.bind_values += bind_values - - attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - add_relations_to_bind_values(attributes) - - predicate_builder.build_from_hash(attributes) - else - [opts] - end - end - - def create_binds(opts) - bindable, non_binds = opts.partition do |column, value| - case value - when String, Integer, ActiveRecord::StatementCache::Substitute - @klass.columns_hash.include? column.to_s - else - false - end - end - - association_binds, non_binds = non_binds.partition do |column, value| - value.is_a?(Hash) && association_for_table(column) - end - - new_opts = {} - binds = [] - - bindable.each do |(column,value)| - binds.push [@klass.columns_hash[column.to_s], value] - new_opts[column] = connection.substitute_at(column) - end - - association_binds.each do |(column, value)| - association_relation = association_for_table(column).klass.send(:relation) - association_new_opts, association_bind = association_relation.send(:create_binds, value) - new_opts[column] = association_new_opts - binds += association_bind - end - - non_binds.each { |column,value| new_opts[column] = value } - - [new_opts, binds] - end - def association_for_table(table_name) table_name = table_name.to_s @klass._reflect_on_association(table_name) || @@ -998,7 +932,8 @@ module ActiveRecord end def build_from - opts, name = from_value + opts = from_clause.value + name = from_clause.name case opts when Relation name ||= 'subquery' @@ -1023,13 +958,14 @@ module ActiveRecord raise 'unknown class: %s' % join.class.name end end + buckets.default = [] - association_joins = buckets[:association_join] || [] - stashed_association_joins = buckets[:stashed_join] || [] - join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map(&:strip).uniq + association_joins = buckets[:association_join] + stashed_association_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + custom_join_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) join_dependency = ActiveRecord::Associations::JoinDependency.new( @klass, @@ -1049,22 +985,33 @@ module ActiveRecord manager end - def build_select(arel, selects) - if !selects.empty? - expanded_select = selects.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field - end - end + def convert_join_strings_to_ast(table, joins) + joins + .flatten + .reject(&:blank?) + .map { |join| table.create_string_join(Arel.sql(join)) } + end - arel.project(*expanded_select) + def build_select(arel) + if select_values.any? + arel.project(*arel_columns(select_values.uniq)) else arel.project(@klass.arel_table[Arel.star]) end end + def arel_columns(columns) + 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 + def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? @@ -1083,10 +1030,6 @@ module ActiveRecord end end - def array_of_strings?(o) - o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } - end - def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) @@ -1154,18 +1097,18 @@ module ActiveRecord end end - # This function is recursive just for better readablity. - # #where argument doesn't support more than one level nested hash in real world. - def add_relations_to_bind_values(attributes) - if attributes.is_a?(Hash) - attributes.each_value do |value| - if value.is_a?(ActiveRecord::Relation) - self.bind_values += value.bind_values - else - add_relations_to_bind_values(value) - end - end - end + def new_where_clause + Relation::WhereClause.empty + end + alias new_having_clause new_where_clause + + def where_clause_factory + @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) + end + alias having_clause_factory where_clause_factory + + def new_from_clause + Relation::FromClause.empty end end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb new file mode 100644 index 0000000000..14e1bf89fa --- /dev/null +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class Relation + module RecordFetchWarning + # When this module is prepended to ActiveRecord::Relation and + # `config.active_record.warn_on_records_fetched_greater_than` is + # set to an integer, if the number of records a query returns is + # greater than the value of `warn_on_records_fetched_greater_than`, + # a warning is logged. This allows for the detection of queries that + # return a large number of records, which could cause memory bloat. + # + # In most cases, fetching large number of records can be performed + # efficiently using the ActiveRecord::Batches methods. + # See active_record/lib/relation/batches.rb for more information. + def exec_queries + QueryRegistry.reset + + super.tap do + if logger && warn_on_records_fetched_greater_than + if @records.length > warn_on_records_fetched_greater_than + logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}" + end + end + end + end + + ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + payload = args.last + + QueryRegistry.queries << payload[:sql] + end + + class QueryRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries + + def initialize + reset + end + + def reset + @queries = [] + end + end + end + end +end + +ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 01bddea6c9..70da37fa84 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -32,7 +32,7 @@ module ActiveRecord elsif other spawn.merge!(other) else - self + raise ArgumentError, "invalid argument: #{other.inspect}." end end @@ -58,9 +58,6 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) - if onlies.any? { |o| o == :where } - onlies << :bind - end relation_with values.slice(*onlies) end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb new file mode 100644 index 0000000000..1f000b3f0f --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -0,0 +1,173 @@ +module ActiveRecord + class Relation + class WhereClause # :nodoc: + attr_reader :binds + + delegate :any?, :empty?, to: :predicates + + def initialize(predicates, binds) + @predicates = predicates + @binds = binds + end + + def +(other) + WhereClause.new( + predicates + other.predicates, + binds + other.binds, + ) + end + + def merge(other) + WhereClause.new( + predicates_unreferenced_by(other) + other.predicates, + non_conflicting_binds(other) + other.binds, + ) + end + + def except(*columns) + WhereClause.new( + predicates_except(columns), + binds_except(columns), + ) + end + + def or(other) + if empty? + self + elsif other.empty? + other + else + WhereClause.new( + [ast.or(other.ast)], + binds + other.binds + ) + end + end + + def to_h(table_name = nil) + equalities = predicates.grep(Arel::Nodes::Equality) + if table_name + equalities = equalities.select do |node| + node.left.relation.name == table_name + end + end + + binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h + + equalities.map { |node| + name = node.left.name + [name, binds.fetch(name.to_s) { + case node.right + when Array then node.right.map(&:val) + when Arel::Nodes::Casted, Arel::Nodes::Quoted + node.right.val + end + }] + }.to_h + end + + def ast + Arel::Nodes::And.new(predicates_with_wrapped_sql_literals) + end + + def ==(other) + other.is_a?(WhereClause) && + predicates == other.predicates && + binds == other.binds + end + + def invert + WhereClause.new(inverted_predicates, binds) + end + + def self.empty + new([], []) + end + + protected + + attr_reader :predicates + + def referenced_columns + @referenced_columns ||= begin + equality_nodes = predicates.select { |n| equality_node?(n) } + Set.new(equality_nodes, &:left) + end + end + + private + + def predicates_unreferenced_by(other) + predicates.reject do |n| + equality_node?(n) && other.referenced_columns.include?(n.left) + end + end + + def equality_node?(node) + node.respond_to?(:operator) && node.operator == :== + end + + def non_conflicting_binds(other) + conflicts = referenced_columns & other.referenced_columns + conflicts.map! { |node| node.name.to_s } + binds.reject { |attr| conflicts.include?(attr.name) } + end + + def inverted_predicates + predicates.map { |node| invert_predicate(node) } + end + + def invert_predicate(node) + case node + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' + when Arel::Nodes::In + Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::Equality + Arel::Nodes::NotEqual.new(node.left, node.right) + when String + Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node)) + else + Arel::Nodes::Not.new(node) + end + end + + def predicates_except(columns) + predicates.reject do |node| + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + end + end + + def binds_except(columns) + binds.reject do |attr| + columns.include?(attr.name) + end + end + + def predicates_with_wrapped_sql_literals + non_empty_predicates.map do |node| + if Arel::Nodes::Equality === node + node + else + wrap_sql_literal(node) + end + end + end + + def non_empty_predicates + predicates - [''] + end + + def wrap_sql_literal(node) + if ::String === node + node = Arel.sql(node) + end + Arel::Nodes::Grouping.new(node) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb new file mode 100644 index 0000000000..0430922be3 --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -0,0 +1,34 @@ +module ActiveRecord + class Relation + class WhereClauseFactory + def initialize(klass, predicate_builder) + @klass = klass + @predicate_builder = predicate_builder + end + + def build(opts, other) + binds = [] + + case opts + when String, Array + parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + when Hash + attributes = predicate_builder.resolve_column_aliases(opts) + attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + + attributes, binds = predicate_builder.create_binds(attributes) + + parts = predicate_builder.build_from_hash(attributes) + else + parts = [opts] + end + + WhereClause.new(parts, binds) + end + + protected + + attr_reader :klass, :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 3a3e65ef32..500c478e65 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -81,7 +81,7 @@ module ActiveRecord def cast_values(type_overrides = {}) # :nodoc: types = columns.map { |name| column_type(name, type_overrides) } result = rows.map do |values| - types.zip(values).map { |type, value| type.type_cast_from_database(value) } + types.zip(values).map { |type, value| type.deserialize(value) } end columns.one? ? result.map!(&:first) : result diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 768a72a947..ba75ffa5a1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,28 +3,23 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - def quote_value(value, column) #:nodoc: - connection.quote(value, column) - end - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. def sanitize(object) #:nodoc: connection.quote(object) end + alias_method :quote_value, :sanitize protected - # Accepts an array, hash, or string of SQL conditions and sanitizes + # Accepts an array or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'" # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" def sanitize_sql_for_conditions(condition, table_name = self.table_name) return nil if condition.blank? case condition when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) else condition end end @@ -78,7 +73,7 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).type_cast_for_database(value) + value = type_for_attribute(attr.to_s).serialize(value) "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end @@ -124,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 @@ -156,7 +151,7 @@ module ActiveRecord # TODO: Deprecate this def quoted_id - self.class.quote_value(id, column_for_attribute(self.class.primary_key)) + self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..c5910fa1ad 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -1,5 +1,4 @@ require 'stringio' -require 'active_support/core_ext/big_decimal' module ActiveRecord # = Active Record Schema Dumper @@ -105,7 +104,10 @@ HEADER end def table(table, stream) - columns = @connection.columns(table) + columns = @connection.columns(table).map do |column| + column.instance_variable_set(:@table_name, table) + column + end begin tbl = StringIO.new @@ -128,6 +130,10 @@ HEADER tbl.print ", id: false" end tbl.print ", force: :cascade" + + table_options = @connection.table_options(table) + tbl.print ", options: #{table_options.inspect}" unless table_options.blank? + tbl.puts " do |t|" # then dump all non-primary key columns @@ -165,11 +171,11 @@ HEADER tbl.puts end + indexes(table, tbl) + tbl.puts " end" tbl.puts - indexes(table, tbl) - tbl.rewind stream.print tbl.read rescue => e @@ -185,8 +191,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - "add_index #{remove_prefix_and_suffix(index.table).inspect}", - index.columns.inspect, + "t.index #{index.columns.inspect}", "name: #{index.name.inspect}", ] statement_parts << 'unique: true' if index.unique @@ -200,11 +205,10 @@ HEADER statement_parts << "using: #{index.using.inspect}" if index.using statement_parts << "type: #{index.type.inspect}" if index.type - " #{statement_parts.join(', ')}" + " #{statement_parts.join(', ')}" end stream.puts add_index_statements.sort.join("\n") - stream.puts end end 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.rb b/activerecord/lib/active_record/scoping.rb index 3e43591672..f049b658c4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,22 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, base_class.to_s) + ScopeRegistry.value_for(:current_scope, self.to_s) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) + end + + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + all.scope_for_create + end + + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 18190cb535..3590b8846e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -33,6 +33,11 @@ module ActiveRecord block_given? ? relation.scoping { yield } : relation end + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + super || default_scopes.any? || respond_to?(:default_scope) + end + def before_remove_const #:nodoc: self.current_scope = nil end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 35420e6551..7b62626896 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,18 +30,13 @@ module ActiveRecord end def default_scoped # :nodoc: - relation.merge(build_default_scope) - end - - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - all.scope_for_create - end + scope = build_default_scope - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? + if scope + relation.spawn.merge!(scope) + else + relation + end end # Adds a class method for retrieving and querying objects. A \scope @@ -151,11 +146,20 @@ module ActiveRecord extension = Module.new(&block) if block - singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } - scope = scope.extending(extension) if extension + if body.respond_to?(:to_proc) + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { instance_exec(*args, &body) } + scope = scope.extending(extension) if extension + + scope || all + end + else + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension - scope || all + scope || all + end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 23d4292cbb..ca11853da7 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -5,7 +5,7 @@ module ActiveRecord module ClassMethods # Example using has_secure_token # - # # Schema: User(toke:string, auth_token:string) + # # Schema: User(token:string, auth_token:string) # class User < ActiveRecord::Base # has_secure_token # has_secure_token :auth_token @@ -13,37 +13,26 @@ module ActiveRecord # # user = User.new # user.save - # user.token # => "44539a6a59835a4ee9d7b112" - # user.auth_token # => "e2426a93718d1817a43abbaa" + # user.token # => "pX27zsMN2ViQKta1bGfLmVJE" + # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7" # user.regenerate_token # => true # user.regenerate_auth_token # => true # - # SecureRandom is used to generate the 24-character unique token, so collisions are highly unlikely. - # We'll check to see if the generated token has been used already using #exists?, and retry up to 10 - # times to find another unused token. After that a RuntimeError is raised if the problem persists. + # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. # # Note that it's still possible to generate a race condition in the database in the same way that - # validates_presence_of can. You're encouraged to add a unique index in the database to deal with - # this even more unlikely scenario. + # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal + # with this even more unlikely scenario. def has_secure_token(attribute = :token) - # Load securerandom only when has_secure_key is used. - require 'securerandom' - define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(attribute) } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token(attribute)) } + # Load securerandom only when has_secure_token is used. + require 'active_support/core_ext/securerandom' + define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } + before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")} end - def generate_unique_secure_token(attribute) - 10.times do |i| - SecureRandom.hex(12).tap do |token| - if exists?(attribute => token) - raise "Couldn't generate a unique token in 10 attempts!" if i == 9 - else - return token - end - end - end + def generate_unique_secure_token + SecureRandom.base58(24) end end end end - diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index c2484d02ed..89b7e0be82 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,9 +180,9 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - column = klass.columns_hash[name] || Type::Value.new + cast_type = klass.type_for_attribute(name) - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 192a19f05d..95986c820c 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -47,8 +47,8 @@ module ActiveRecord def sql_for(binds, connection) val = @values.dup - binds = binds.dup - @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + binds = connection.prepare_binds_for_database(binds) + @indexes.each { |i| val[i] = connection.quote(binds.shift) } val.join end end @@ -67,21 +67,21 @@ module ActiveRecord end class BindMap # :nodoc: - def initialize(bind_values) + def initialize(bound_attributes) @indexes = [] - @bind_values = bind_values + @bound_attributes = bound_attributes - bind_values.each_with_index do |(_, value), i| - if Substitute === value + bound_attributes.each_with_index do |attr, i| + if Substitute === attr.value @indexes << i end end end def bind(values) - bvs = @bind_values.map(&:dup) - @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } - bvs + bas = @bound_attributes.dup + @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + bas end end @@ -89,7 +89,7 @@ module ActiveRecord def self.create(connection, block = Proc.new) relation = block.call Params.new - bind_map = BindMap.new relation.bind_values + bind_map = BindMap.new relation.bound_attributes query_builder = connection.cacheable_query relation.arel new query_builder, bind_map end diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb new file mode 100644 index 0000000000..b3644bf569 --- /dev/null +++ b/activerecord/lib/active_record/suppressor.rb @@ -0,0 +1,54 @@ +module ActiveRecord + # ActiveRecord::Suppressor prevents the receiver from being saved during + # a given block. + # + # For example, here's a pattern of creating notifications when new comments + # are posted. (The notification may in turn trigger an email, a push + # notification, or just appear in the UI somewhere): + # + # class Comment < ActiveRecord::Base + # belongs_to :commentable, polymorphic: true + # after_create -> { Notification.create! comment: self, + # recipients: commentable.recipients } + # end + # + # That's what you want the bulk of the time. New comment creates a new + # Notification. But there may well be off cases, like copying a commentable + # and its comments, where you don't want that. So you'd have a concern + # something like this: + # + # module Copyable + # def copy_to(destination) + # Notification.suppress do + # # Copy logic that creates new comments that we do not want + # # triggering notifications. + # end + # end + # end + module Suppressor + extend ActiveSupport::Concern + + module ClassMethods + def suppress(&block) + SuppressorRegistry.suppressed[name] = true + yield + ensure + SuppressorRegistry.suppressed[name] = false + end + end + + def create_or_update(*args) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + end + + class SuppressorRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_reader :suppressed + + def initialize + @suppressed = {} + end + end +end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 11e33e8dfe..3dd6321a97 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -1,6 +1,7 @@ module ActiveRecord class TableMetadata # :nodoc: delegate :foreign_type, :foreign_key, to: :association, prefix: true + delegate :association_primary_key, to: :association def initialize(klass, arel_table, association = nil) @klass = klass @@ -22,6 +23,14 @@ module ActiveRecord arel_table[column_name] end + def type(column_name) + if klass + klass.type_for_attribute(column_name.to_s) + else + Type::Value.new + end + end + def associated_with?(association_name) klass && klass._reflect_on_association(association_name) end @@ -34,7 +43,7 @@ module ActiveRecord association_klass = association.klass arel_table = association_klass.arel_table else - type_caster = TypeCaster::Connection.new(klass.connection, table_name) + type_caster = TypeCaster::Connection.new(klass, table_name) association_klass = nil arel_table = Arel::Table.new(table_name, type_caster: type_caster) end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 69aceb66b1..683741768b 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -212,7 +212,7 @@ module ActiveRecord load_schema(*args) end - def schema_file(format = ActiveSupport::Base.schema_format) + def schema_file(format = ActiveRecord::Base.schema_format) case format when :ruby File.join(db_dir, "schema.rb") diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index eafbb2c249..673386f0d9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -59,6 +59,7 @@ module ActiveRecord args = prepare_command_options('mysqldump') args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) + args.concat(["--routines"]) args.concat(["#{configuration['database']}"]) unless Kernel.system(*args) $stderr.puts "Could not dump the database structure. "\ @@ -130,15 +131,21 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def prepare_command_options(command) - args = [command] - args.concat(['--user', configuration['username']]) if configuration['username'] - args << "--password=#{configuration['password']}" if configuration['password'] - args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] - configuration.slice('host', 'port', 'socket').each do |k, v| - args.concat([ "--#{k}", v.to_s ]) if v - end - - args + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'password' => '--password', + 'encoding' => '--default-character-set', + 'sslca' => '--ssl-ca', + 'sslcert' => '--ssl-cert', + 'sslcapath' => '--ssl-capath', + 'sslcipher' => '--ssh-cipher', + 'sslkey' => '--ssl-key' + }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact + + [command, *args] end end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index ce1de4b76e..d7da95c8a9 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -46,7 +46,15 @@ module ActiveRecord def structure_dump(filename) set_psql_env - search_path = configuration['schema_search_path'] + + search_path = case ActiveRecord::Base.dump_schemas + when :schema_search_path + configuration['schema_search_path'] + when :all + nil + when String + ActiveRecord::Base.dump_schemas + end unless search_path.blank? search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") end @@ -59,7 +67,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb new file mode 100644 index 0000000000..4352a0ffea --- /dev/null +++ b/activerecord/lib/active_record/touch_later.rb @@ -0,0 +1,50 @@ +module ActiveRecord + # = Active Record Touch Later + module TouchLater + extend ActiveSupport::Concern + + included do + before_commit_without_transaction_enrollment :touch_deferred_attributes + end + + def touch_later(*names) # :nodoc: + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + + @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model + @_defer_touch_attrs |= names + @_touch_time = current_time_from_proper_timezone + + surreptitiously_touch @_defer_touch_attrs + self.class.connection.add_transaction_record self + end + + def touch(*names, time: nil) # :nodoc: + if has_defer_touch_attrs? + names |= @_defer_touch_attrs + end + super(*names, time: time) + end + + private + def surreptitiously_touch(attrs) + attrs.each { |attr| write_attribute attr, @_touch_time } + clear_attribute_changes attrs + end + + def touch_deferred_attributes + if has_defer_touch_attrs? && persisted? + @_touching_delayed_records = true + touch(*@_defer_touch_attrs, time: @_touch_time) + @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + end + end + + def has_defer_touch_attrs? + defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? + end + + def touching_delayed_records? + defined?(@_touching_delayed_records) && @_touching_delayed_records + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cef50029b..6f2def0df1 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -7,6 +7,10 @@ module ActiveRecord included do define_callbacks :commit, :rollback, + :before_commit, + :before_commit_without_transaction_enrollment, + :commit_without_transaction_enrollment, + :rollback_without_transaction_enrollment, scope: [:kind, :name] end @@ -192,9 +196,9 @@ 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 # @@ -206,6 +210,11 @@ module ActiveRecord connection.transaction(options, &block) end + def before_commit(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:before_commit, :before, *args, &block) + end + # This callback is called after a record has been created, updated, or destroyed. # # You can specify that the callback should only be fired by a certain action with @@ -218,8 +227,6 @@ module ActiveRecord # after_commit :do_foo_bar, on: [:create, :update] # after_commit :do_bar_baz, on: [:update, :destroy] # - # Note that transactional fixtures do not play well with this feature. Please - # use the +test_after_commit+ gem to have these hooks fired in tests. def after_commit(*args, &block) set_options_for_callbacks!(args) set_callback(:commit, :after, *args, &block) @@ -233,6 +240,21 @@ module ActiveRecord set_callback(:rollback, :after, *args, &block) end + def before_commit_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:before_commit_without_transaction_enrollment, :before, *args, &block) + end + + def after_commit_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:commit_without_transaction_enrollment, :after, *args, &block) + end + + def after_rollback_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:rollback_without_transaction_enrollment, :after, *args, &block) + end + def raise_in_transactional_callbacks ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.') true @@ -296,20 +318,31 @@ module ActiveRecord clear_transaction_record_state end + def before_committed! # :nodoc: + run_callbacks :before_commit_without_transaction_enrollment + run_callbacks :before_commit + end + # Call the +after_commit+ callbacks. # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. - def committed!(should_run_callbacks = true) #:nodoc: - _run_commit_callbacks if should_run_callbacks && destroyed? || persisted? + def committed!(should_run_callbacks: true) #:nodoc: + if should_run_callbacks && destroyed? || persisted? + run_callbacks :commit_without_transaction_enrollment + run_callbacks :commit + end ensure force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # 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: - _run_rollback_callbacks if should_run_callbacks + def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: + if should_run_callbacks + run_callbacks :rollback + run_callbacks :rollback_without_transaction_enrollment + end ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state @@ -318,9 +351,13 @@ module ActiveRecord # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks # can be called. def add_to_transaction - if self.class.connection.add_transaction_record(self) - remember_transaction_record_state + if has_transactional_callbacks? + self.class.connection.add_transaction_record(self) + else + sync_with_transaction_state + set_transaction_state(self.class.connection.transaction_state) end + remember_transaction_record_state end # Executes +method+ within a transaction and captures its return value as a @@ -375,10 +412,14 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - thaw unless restore_state[:frozen?] + thaw @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - write_attribute(self.class.primary_key, restore_state[:id]) if self.class.primary_key + pk = self.class.primary_key + if pk && read_attribute(pk) != restore_state[:id] + write_attribute(pk, restore_state[:id]) + end + freeze if restore_state[:frozen?] end end end @@ -401,5 +442,43 @@ module ActiveRecord end end end + + private + + def set_transaction_state(state) # :nodoc: + @transaction_state = state + end + + def has_transactional_callbacks? # :nodoc: + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? + end + + # Updates the attributes on this particular ActiveRecord object so that + # if it's associated with a transaction, then the state of the ActiveRecord + # object will be updated to reflect the current state of the transaction + # + # The @transaction_state variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required) + # Each ActiveRecord object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the ActiveRecord object + # as appropriate. + # + # Since ActiveRecord objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the ActiveRecord object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state) + end + + def update_attributes_from_transaction_state(transaction_state) + if transaction_state && transaction_state.finalized? + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state + end + end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 250e8d5b23..2c0cda69d0 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,7 +1,4 @@ -require 'active_record/type/decorator' -require 'active_record/type/mutable' -require 'active_record/type/numeric' -require 'active_record/type/time_value' +require 'active_record/type/helpers' require 'active_record/type/value' require 'active_record/type/big_integer' @@ -19,5 +16,51 @@ require 'active_record/type/text' require 'active_record/type/time' require 'active_record/type/unsigned_integer' +require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' + +module ActiveRecord + module Type + @registry = AdapterSpecificRegistry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, adapter: current_adapter_name, **kwargs) # :nodoc: + registry.lookup(*args, adapter: adapter, **kwargs) + end + + private + + def current_adapter_name + ActiveRecord::Base.connection.adapter_name.downcase.to_sym + end + end + + register(:big_integer, Type::BigInteger, override: false) + register(:binary, Type::Binary, override: false) + register(:boolean, Type::Boolean, override: false) + register(:date, Type::Date, override: false) + register(:date_time, Type::DateTime, override: false) + register(:decimal, Type::Decimal, override: false) + register(:float, Type::Float, override: false) + register(:integer, Type::Integer, override: false) + register(:string, Type::String, override: false) + register(:text, Type::Text, override: false) + register(:time, Type::Time, override: false) + end +end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb new file mode 100644 index 0000000000..5f71b3cb94 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,142 @@ +module ActiveRecord + # :stopdoc: + module Type + class AdapterSpecificRegistry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << Registration.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = registrations + .select { |r| r.matches?(symbol, *args) } + .max + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) + end + + protected + + attr_reader :registrations + end + + class Registration + def initialize(name, block, adapter: nil, override: nil) + @name = name + @block = block + @adapter = adapter + @override = override + end + + def call(_registry, *args, adapter: nil, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name && matches_adapter?(**kwargs) + end + + def <=>(other) + if conflicts_with?(other) + raise TypeConflictError.new("Type #{name} was registered for all + adapters, but shadows a native type with + the same name for #{other.adapter}".squish) + end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result + end + + def priority_except_adapter + priority & 0b111111100 + end + + private + + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end + + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end + + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end + + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end + end + + class DecorationRegistration < Registration + def initialize(options, klass, adapter: nil) + @options = options + @klass = klass + @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_adapter?(**kwargs) && matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 005a48ef0d..0baf8c63ad 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -9,7 +9,7 @@ module ActiveRecord true end - def type_cast(value) + def cast(value) if value.is_a?(Data) value.to_s else @@ -17,13 +17,13 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? Data.new(super) end def changed_in_place?(raw_old_value, value) - old_value = type_cast_from_database(raw_old_value) + old_value = deserialize(raw_old_value) old_value != value end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index d90a6069b7..3ceab59ebb 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,14 +1,12 @@ module ActiveRecord module Type class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + def type :date end - def klass - ::Date - end - def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -41,6 +39,11 @@ module ActiveRecord ::Date.new(year, mon, mday) rescue nil end end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 0a737815bc..a5199959b9 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,26 +1,15 @@ module ActiveRecord module Type class DateTime < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) def type :datetime end - def type_cast_for_database(value) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - - if value.acts_like?(:time) - if value.respond_to?(zone_conversion_method) - value.send(zone_conversion_method) - else - value - end - else - super - end - end - private def cast_value(string) @@ -42,6 +31,14 @@ module ActiveRecord new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 7b2bee2c42..867b5f75c7 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Decimal < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :decimal diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb deleted file mode 100644 index 9fce38ea44..0000000000 --- a/activerecord/lib/active_record/type/decorator.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActiveRecord - module Type - module Decorator # :nodoc: - def init_with(coder) - @subtype = coder['subtype'] - __setobj__(@subtype) - end - - def encode_with(coder) - coder['subtype'] = __getobj__ - end - end - end -end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index 42eb44b9a9..d88482b85d 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -1,18 +1,24 @@ module ActiveRecord module Type class Float < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :float end - alias type_cast_for_database type_cast + alias serialize cast private def cast_value(value) - value.to_f + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end end end end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 82d9327fc0..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,12 +1,18 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - def alias_type(type, alias_type) register_type(type) { |_, *args| lookup(alias_type, *args) } end + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + private def perform_fetch(type, *args, &block) diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb new file mode 100644 index 0000000000..634d417d13 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_record/type/helpers/accepts_multiparameter_time' +require 'active_record/type/helpers/numeric' +require 'active_record/type/helpers/mutable' +require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..be571fc1c7 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send( + ActiveRecord::Base.default_timezone, + *values + ) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb new file mode 100644 index 0000000000..88a9099277 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Helpers + module Mutable # :nodoc: + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb new file mode 100644 index 0000000000..a755a02a59 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module Helpers + module Numeric # :nodoc: + def cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb new file mode 100644 index 0000000000..7eb41557cb --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/time_value.rb @@ -0,0 +1,58 @@ +module ActiveRecord + module Type + module Helpers + module TimeValue # :nodoc: + def serialize(value) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) + end + + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index fc260a081a..c5040c6d3b 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -1,7 +1,11 @@ module ActiveRecord module Type class Integer < Value # :nodoc: - include Numeric + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 def initialize(*) super @@ -12,13 +16,19 @@ module ActiveRecord :integer end - alias type_cast_for_database type_cast - - def type_cast_from_database(value) + def deserialize(value) return if value.nil? value.to_i end + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + protected attr_reader :range @@ -30,26 +40,27 @@ module ActiveRecord when true then 1 when false then 0 else - result = value.to_i rescue nil - ensure_in_range(result) if result - result + value.to_i rescue nil end end def ensure_in_range(value) unless range.cover?(value) - raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}" + raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" end end def max_value - limit = self.limit || 4 - 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign + 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign end def min_value -max_value end + + def _limit + self.limit || DEFAULT_LIMIT + end end end end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb deleted file mode 100644 index 066617ea59..0000000000 --- a/activerecord/lib/active_record/type/mutable.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module Type - module Mutable # :nodoc: - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - - # +raw_old_value+ will be the `_before_type_cast` version of the - # value (likely a string). +new_value+ will be the current, type - # cast value. - def changed_in_place?(raw_old_value, new_value) - raw_old_value != type_cast_for_database(new_value) - end - end - end -end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb deleted file mode 100644 index 674f996f38..0000000000 --- a/activerecord/lib/active_record/type/numeric.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - super || number_to_non_number?(old_value, new_value_before_type_cast) - end - - private - - def number_to_non_number?(old_value, new_value_before_type_cast) - old_value != nil && non_numeric_string?(new_value_before_type_cast) - end - - def non_numeric_string?(value) - # 'wibble'.to_i will give zero, we want to make sure - # that we aren't marking int zero to string zero as - # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ - end - end - end -end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 3cac03464e..ea3e0d6a45 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,8 +1,7 @@ module ActiveRecord module Type class Serialized < DelegateClass(Type::Value) # :nodoc: - include Mutable - include Decorator + include Helpers::Mutable attr_reader :subtype, :coder @@ -12,7 +11,7 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) + def deserialize(value) if default_value?(value) value else @@ -20,32 +19,28 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? unless default_value?(value) super coder.dump(value) end end + def inspect + Kernel.instance_method(:inspect).bind(self).call + end + def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) + raw_new_value = serialize(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) end def accessor ActiveRecord::Store::IndifferentHashAccessor end - def init_with(coder) - @coder = coder['coder'] - super - end - - def encode_with(coder) - coder['coder'] = @coder - super - end - private def default_value?(value) diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index cf95e25be0..2662b7e874 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -11,7 +11,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) case value when ::Numeric, ActiveSupport::Duration then value.to_s when ::String then ::String.new(value) @@ -21,10 +21,6 @@ module ActiveRecord end end - def text? - true - end - private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 41f7d97f0c..19a10021bc 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,12 +1,28 @@ module ActiveRecord module Type class Time < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) def type :time end + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb deleted file mode 100644 index d611d72dd4..0000000000 --- a/activerecord/lib/active_record/type/time_value.rb +++ /dev/null @@ -1,38 +0,0 @@ -module ActiveRecord - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 60ae47db3d..6b9d147ecc 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -1,48 +1,50 @@ module ActiveRecord module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. They are only - # used when dumping schema. - def initialize(options = {}) - options.assert_valid_keys(:precision, :scale, :limit) - @precision = options[:precision] - @scale = options[:scale] - @limit = options[:limit] + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit end - # The simplified type that this object represents. Returns a symbol such - # as +:string+ or +:integer+ - def type; end + def type # :nodoc: + end - # Type casts a string from the database into the appropriate ruby type. - # Classes which do not need separate type casting behavior for database - # and user provided values should override +cast_value+ instead. - def type_cast_from_database(value) - type_cast(value) + # Converts a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) end # Type casts a value from user input (e.g. from a setter). This value may - # be a string from the form builder, or an already type cast value - # provided manually to a setter. + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. # - # Classes which do not need separate type casting behavior for database - # and user provided values should override +type_cast+ or +cast_value+ - # instead. - def type_cast_from_user(value) - type_cast(value) + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? end - # Cast a value from the ruby type to a type that the database knows how + # Casts a value from the ruby type to a type that the database knows how # to understand. The returned value from this method should be a # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or - # +nil+ - def type_cast_for_database(value) + # +nil+. + def serialize(value) value end - # Type cast a value for schema dumping. This method is private, as we are + # Type casts a value for schema dumping. This method is private, as we are # hoping to remove it entirely. def type_cast_for_schema(value) # :nodoc: value.inspect @@ -50,21 +52,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. - def text? # :nodoc: - false - end - - def number? # :nodoc: - false - end - def binary? # :nodoc: false end - def klass # :nodoc: - end - # Determines whether a value has changed for dirty checking. +old_value+ # and +new_value+ will always be type-cast. Types should not need to # override this method. @@ -73,10 +64,23 @@ module ActiveRecord end # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. This method should not be overridden - # directly. Types which return a mutable value should include - # +Type::Mutable+, which will define this method. - def changed_in_place?(*) + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) false end @@ -89,14 +93,9 @@ module ActiveRecord private - def type_cast(value) - cast_value(value) unless value.nil? - end - # Convenience method for types which do not need separate type casting - # behavior for user and database inputs. Called by - # +type_cast_from_database+ and +type_cast_from_user+ for all values - # except +nil+. + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. def cast_value(value) # :doc: value end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 9e4a130b40..3878270770 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,34 +1,29 @@ module ActiveRecord module TypeCaster class Connection - def initialize(connection, table_name) - @connection = connection + def initialize(klass, table_name) + @klass = klass @table_name = table_name end def type_cast_for_database(attribute_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - type = type_for(attribute_name) - type.type_cast_for_database(value) + column = column_for(attribute_name) + connection.type_cast_from_column(column, value) end protected - attr_reader :connection, :table_name + attr_reader :table_name + delegate :connection, to: :@klass private - def type_for(attribute_name) + def column_for(attribute_name) if connection.schema_cache.table_exists?(table_name) - column_for(attribute_name).cast_type - else - Type::Value.new + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] end end - - def column_for(attribute_name) - connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] - end end end end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 03c9e8ff83..4b1941351c 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -8,7 +8,7 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) type = types.type_for_attribute(attr_name.to_s) - type.type_cast_for_database(value) + type.serialize(value) end protected diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f27adc9c40..e227212827 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -40,7 +40,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise_record_invalid + perform_validations(options) ? super : raise_validation_error end # Runs all the validations within the specified context. Returns +true+ if @@ -61,21 +61,9 @@ module ActiveRecord alias_method :validate, :valid? - # Runs all the validations within the specified context. Returns +true+ if - # no errors are found, raises +RecordInvalid+ otherwise. - # - # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if - # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. - # - # Validations with no <tt>:on</tt> option will run no matter the context. Validations with - # some <tt>:on</tt> option will only run in the specified context. - def validate!(context = nil) - valid?(context) || raise_record_invalid - end - protected - def raise_record_invalid + def raise_validation_error raise(RecordInvalid.new(self)) end diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index ef5a6cbbe7..5991fbad8e 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,11 +2,23 @@ module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: def validate_each(record, attribute, association_or_value) + return unless should_validate?(record) || associations_are_dirty?(record) if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? association_or_value = association_or_value.target.reject(&:marked_for_destruction?) end super end + + def associations_are_dirty?(record) + attributes.any? do |attribute| + value = record.read_attribute_for_validation(attribute) + if value.respond_to?(:loaded?) && value.loaded? + value.target.any?(&:marked_for_destruction?) + else + false + end + end + end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 61b30749d9..a9b791397b 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,6 +2,7 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate(record) + return unless should_validate?(record) super attributes.each do |attribute| next unless record.class._reflect_on_association(attribute) @@ -42,6 +43,10 @@ module ActiveRecord # deletes the associated object, thus putting the parent object into an invalid # state. # + # NOTE: This validation will not fail while using it with an association + # if the latter was assigned but not valid. If you want to ensure that + # it is both present and valid, you also need to use +validates_associated+. + # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index f52f91e89c..5106f4e127 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,6 +11,7 @@ module ActiveRecord end def validate_each(record, attribute, value) + return unless should_validate?(record) finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) @@ -59,7 +60,8 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.type_for_attribute(attribute_name).type_cast_for_database(value) + cast_type = klass.type_for_attribute(attribute_name) + value = cast_type.serialize(value) value = klass.connection.type_cast(value) if value.is_a?(String) && column.limit value = value.to_s[0, column.limit] @@ -67,13 +69,15 @@ module ActiveRecord value = Arel::Nodes::Quoted.new(value) - comparison = if !options[:case_sensitive] && value && column.text? + comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end klass.unscoped.where(comparison) + rescue RangeError + klass.none end def scope_relation(record, table, relation) diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 7a3c6f5e95..0d57de4d65 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -14,10 +14,9 @@ module ActiveRecord protected attr_reader :migration_action, :join_tables - # sets the default migration template that is being used for the generation of the migration - # depending on the arguments which would be sent out in the command line, the migration template - # and the table name instance variables are setup. - + # Sets the default migration template that is being used for the generation of the migration. + # Depending on command line arguments, the migration template and the table name instance + # variables are set up. def set_local_assigns! @migration_template = "migration.rb" case file_name @@ -56,6 +55,8 @@ module ActiveRecord attributes.select { |a| !a.reference? && a.has_index? } end + # A migration file name can only contain underscores (_), lowercase characters, + # and numbers 0-9. Any other file name will raise an IllegalMigrationNameError. def validate_file_name! unless file_name =~ /^[_a-z0-9]+$/ raise IllegalMigrationNameError.new(file_name) diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fd94a2d038..5b3e57dcf6 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -4,6 +4,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <% if attribute.password_digest? -%> t.string :password_digest<%= attribute.inject_options %> +<% elsif attribute.token? -%> + t.string :<%= attribute.name %><%= attribute.inject_options %> <% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> @@ -12,6 +14,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration t.timestamps <% end -%> end +<% attributes.select(&:token?).each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true +<% end -%> <% attributes_with_index.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index ae9c74fd05..23a377db6a 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -4,6 +4,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <%- if attribute.reference? -%> add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- elsif attribute.token? -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 539d969fce..55dc65c8ad 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -3,6 +3,9 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> +<% attributes.select(&:token?).each do |attribute| -%> + has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %> +<% end -%> <% if attributes.any?(&:password_digest?) -%> has_secure_password <% end -%> diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 64cde143a1..49a68fb94c 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -22,15 +22,14 @@ module ActiveRecord end def primary_key(table) - @primary_keys[table] + @primary_keys[table] || "id" end def merge_column(table_name, name, sql_type = nil, options = {}) @columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new( name.to_s, options[:default], - lookup_cast_type(sql_type.to_s), - sql_type.to_s, + fetch_type_metadata(sql_type), options[:null]) end @@ -38,6 +37,10 @@ module ActiveRecord @columns[table_name] end + def table_exists?(*) + true + end + def active? true end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 99e3d7021d..1712ff0ac6 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -193,7 +193,7 @@ module ActiveRecord author = Author.create!(name: 'john') Post.create!(author: author, title: 'foo', body: 'bar') query = author.posts.where(title: 'foo').select(:title) - assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) + assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) assert_equal "foo", @connection.select_value(query) @@ -203,7 +203,7 @@ module ActiveRecord def test_select_methods_passing_a_relation Post.create!(title: 'foo', body: 'bar') query = Post.where(title: 'foo').select(:title) - assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) + assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) assert_equal "foo", @connection.select_value(query) @@ -226,7 +226,7 @@ module ActiveRecord end class AdapterTestWithoutTransaction < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Klass < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 6577d56240..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 @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end + def test_index_in_create + def (ActiveRecord::Base.connection).table_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual + end + + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy + end + assert_equal expected, actual + end + def test_drop_table assert_equal "DROP TABLE `people`", drop_table(:people) end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 340fc95503..98d44315dd 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -1,7 +1,6 @@ require "cases/helper" -require 'models/person' -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 new file mode 100644 index 0000000000..f2117a97e6 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlCharsetCollationTest < ActiveRecord::MysqlTestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index ce01b16362..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 @@ -94,7 +94,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase with_example_table do @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -106,10 +106,10 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_exec_typecasts_bind_vals with_example_table do @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - column = @connection.columns('ex').find { |col| col.name == 'id' } + bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + 'SELECT id, data FROM ex WHERE id = ?', nil, [bind]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -145,6 +145,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_strict_mode_specified_default + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default})) + global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows + end + end + def test_mysql_set_session_variable run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb index e972d6b330..743f6436e4 100644 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -1,7 +1,7 @@ require "cases/helper" -class MysqlConsistencyTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class MysqlConsistencyTest < ActiveRecord::MysqlTestCase + self.use_transactional_tests = false class Consistency < ActiveRecord::Base self.table_name = "mysql_consistency" diff --git a/activerecord/test/cases/adapters/mysql/datetime_test.rb b/activerecord/test/cases/adapters/mysql/datetime_test.rb deleted file mode 100644 index ae00f4e131..0000000000 --- a/activerecord/test/cases/adapters/mysql/datetime_test.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'cases/helper' - -if mysql_56? - class DateTimeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - class Foo < ActiveRecord::Base; end - - def test_default_datetime_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime - assert_nil activerecord_column_option('foos', 'created_at', 'precision') - end - - def test_datetime_data_type_with_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1 - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5 - assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_timestamps_helper_with_custom_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_passing_precision_to_datetime_does_not_set_limit - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_nil activerecord_column_option('foos', 'created_at', 'limit') - assert_nil activerecord_column_option('foos', 'updated_at', 'limit') - end - - def test_invalid_datetime_precision_raises_error - assert_raises ActiveRecord::ActiveRecordError do - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 7 - end - end - end - - def test_mysql_agrees_with_activerecord_about_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_equal 4, mysql_datetime_precision('foos', 'created_at') - assert_equal 4, mysql_datetime_precision('foos', 'updated_at') - end - - def test_formatting_datetime_according_to_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.datetime :created_at, precision: 0 - t.datetime :updated_at, precision: 4 - end - date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) - Foo.create!(created_at: date, updated_at: date) - assert foo = Foo.find_by(created_at: date) - assert_equal date.to_s, foo.created_at.to_s - assert_equal date.to_s, foo.updated_at.to_s - assert_equal 000000, foo.created_at.usec - assert_equal 999900, foo.updated_at.usec - end - - private - - def mysql_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] - end - - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end - end -end 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 85db8f4614..b804cb45b9 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -1,11 +1,10 @@ -# encoding: utf-8 require "cases/helper" require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class MysqlAdapterTest < ActiveRecord::TestCase + class MysqlAdapterTest < ActiveRecord::MysqlTestCase include DdlHelper def setup @@ -16,7 +15,7 @@ module ActiveRecord assert_raise ActiveRecord::NoDatabaseError do configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') connection = ActiveRecord::Base.mysql_connection(configuration) - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end @@ -111,7 +110,7 @@ module ActiveRecord result = @conn.exec_query('SELECT status FROM ex') - assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status']) + assert_equal 2, result.column_types['status'].deserialize(result.last['status']) end end @@ -129,10 +128,10 @@ module ActiveRecord private def insert(ctx, data, table='ex') - binds = data.map { |name, value| - [ctx.columns(table).find { |x| x.name == name }, value] + binds = data.map { |name, value| + Relation::QueryAttribute.new(name, value, Type::Value.new) } - columns = binds.map(&:first).map(&:name) + columns = binds.map(&:name) sql = "INSERT INTO #{table} (#{columns.join(", ")}) VALUES (#{(['?'] * columns.length).join(', ')})" diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index a2206153e9..a296cf9d31 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -1,21 +1,15 @@ 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 - end - end + def test_type_cast_false + assert_equal 0, @conn.type_cast(false) 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 403f7cbc74..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 @@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #fixtures self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false #activerecord model class with reserved-word table name def test_activerecord_model @@ -139,7 +139,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name def drop_tables_directly(table_names, connection = @connection) table_names.each do |name| - connection.execute("DROP TABLE IF EXISTS `#{name}`") + connection.drop_table name, if_exists: true end end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index ab547747df..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 @@ -22,7 +22,7 @@ module ActiveRecord end teardown do - @connection.execute "drop table if exists mysql_doubles" + @connection.drop_table "mysql_doubles", if_exists: true end class MysqlDouble < ActiveRecord::Base 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 new file mode 100644 index 0000000000..99df6d6cba --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlTableOptionsTest < ActiveRecord::MysqlTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + test "table options with ENGINE" do + @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=MyISAM}, options + end + + test "table options with ROW_FORMAT" do + @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ROW_FORMAT=REDUNDANT}, options + end + + test "table options with CHARSET" do + @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{CHARSET=utf8mb4}, options + end + + test "table options with COLLATE" do + @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{COLLATE=utf8mb4_bin}, options + end +end diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb index 8f521e9181..ed9398a918 100644 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -1,7 +1,7 @@ require "cases/helper" -class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase + self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index e87cd3886a..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 @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end + def test_index_in_create + def (ActiveRecord::Base.connection).table_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual + end + + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy + end + assert_equal expected, actual + end + def test_drop_table assert_equal "DROP TABLE `people`", drop_table(:people) end 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 0e641ba3bf..8575df9e43 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,7 +1,7 @@ require "cases/helper" -class Mysql2BooleanTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false class BooleanType < ActiveRecord::Base self.table_name = "mysql_booleans" diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 09bebf3071..963116f08a 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,7 +1,6 @@ require "cases/helper" -require 'models/person' -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 new file mode 100644 index 0000000000..4fd34def15 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index d261e2db55..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 @@ -22,7 +22,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_raise ActiveRecord::NoDatabaseError do configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') connection = ActiveRecord::Base.mysql2_connection(configuration) - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end @@ -84,6 +84,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_strict_mode_specified_default + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default})) + global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows + end + end + def test_mysql_set_session_variable run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) @@ -122,11 +131,4 @@ class MysqlConnectionTest < ActiveRecord::TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end - - if mysql_56? - def test_quote_time_usec - assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0)) - assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime) - end - end end diff --git a/activerecord/test/cases/adapters/mysql2/datetime_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_test.rb deleted file mode 100644 index ae00f4e131..0000000000 --- a/activerecord/test/cases/adapters/mysql2/datetime_test.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'cases/helper' - -if mysql_56? - class DateTimeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - class Foo < ActiveRecord::Base; end - - def test_default_datetime_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime - assert_nil activerecord_column_option('foos', 'created_at', 'precision') - end - - def test_datetime_data_type_with_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1 - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5 - assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_timestamps_helper_with_custom_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_passing_precision_to_datetime_does_not_set_limit - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_nil activerecord_column_option('foos', 'created_at', 'limit') - assert_nil activerecord_column_option('foos', 'updated_at', 'limit') - end - - def test_invalid_datetime_precision_raises_error - assert_raises ActiveRecord::ActiveRecordError do - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 7 - end - end - end - - def test_mysql_agrees_with_activerecord_about_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.timestamps null: true, precision: 4 - end - assert_equal 4, mysql_datetime_precision('foos', 'created_at') - assert_equal 4, mysql_datetime_precision('foos', 'updated_at') - end - - def test_formatting_datetime_according_to_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| - t.datetime :created_at, precision: 0 - t.datetime :updated_at, precision: 4 - end - date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) - Foo.create!(created_at: date, updated_at: date) - assert foo = Foo.find_by(created_at: date) - assert_equal date.to_s, foo.created_at.to_s - assert_equal date.to_s, foo.updated_at.to_s - assert_equal 000000, foo.created_at.usec - assert_equal 999900, foo.updated_at.usec - end - - private - - def mysql_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] - end - - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end - end -end 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/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 7f97b454bb..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 @@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #fixtures self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false #activerecord model class with reserved-word table name def test_activerecord_model @@ -138,7 +138,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name def drop_tables_directly(table_names, connection = @connection) table_names.each do |name| - connection.execute("DROP TABLE IF EXISTS `#{name}`") + connection.drop_table name, if_exists: true end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 9c49599d34..396f235e77 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,39 +1,42 @@ require "cases/helper" -module ActiveRecord - module ConnectionAdapters - class Mysql2Adapter - 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 connection.table_exists?(smtn) - - config = connection.instance_variable_get(:@config) - original_encoding = config[:encoding] - - config[:encoding] = 'utf8mb4' - connection.initialize_schema_migrations_table - - assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) - ensure - config[:encoding] = original_encoding - end - - private - def connection - @connection ||= ActiveRecord::Base.connection - 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 new file mode 100644 index 0000000000..af121ee7d9 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + test "table options with ENGINE" do + @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=MyISAM}, options + end + + test "table options with ROW_FORMAT" do + @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ROW_FORMAT=REDUNDANT}, options + end + + test "table options with CHARSET" do + @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{CHARSET=utf8mb4}, options + end + + test "table options with COLLATE" do + @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{COLLATE=utf8mb4_bin}, options + end +end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index 8f521e9181..9e06db2519 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,7 +1,7 @@ require "cases/helper" -class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base end 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 77055f5b7a..380a90d765 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,11 +1,9 @@ -# encoding: utf-8 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' @@ -24,11 +22,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase t.hstore :hstores, array: true end end + PgArray.reset_column_information @column = PgArray.columns_hash['tags'] + @type = PgArray.type_for_attribute("tags") end teardown do - @connection.execute 'drop table if exists pg_arrays' + @connection.drop_table 'pg_arrays', if_exists: true disable_extension!('hstore', @connection) end @@ -36,13 +36,11 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array? - assert_not @column.number? - assert_not @column.binary? + assert_not @type.binary? ratings_column = PgArray.columns_hash['ratings'] assert_equal :integer, ratings_column.type assert ratings_column.array? - assert_not ratings_column.number? end def test_default @@ -94,9 +92,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}')) - assert_equal([], @column.type_cast_from_database('{}')) - assert_equal([nil], @column.type_cast_from_database('{NULL}')) + assert_equal(['1', '2', '3'], @type.deserialize('{1,2,3}')) + assert_equal([], @type.deserialize('{}')) + assert_equal([nil], @type.deserialize('{NULL}')) end def test_type_cast_integers @@ -112,8 +110,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema "pg_arrays" - assert_match %r[t.string\s+"tags",\s+array: true], output - assert_match %r[t.integer\s+"ratings",\s+array: true], output + assert_match %r[t\.string\s+"tags",\s+array: true], output + assert_match %r[t\.integer\s+"ratings",\s+array: true], output end def test_select_with_strings @@ -208,16 +206,17 @@ class PostgresqlArrayTest < ActiveRecord::TestCase x = PgArray.create!(tags: tags) x.reload - assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags) + assert_equal x.tags_before_type_cast, PgArray.type_for_attribute('tags').serialize(tags) end 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.type_cast_for_database(strings) - assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + assert_equal %({"hello,",world;}), comma_delim.serialize(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) end def test_mutate_array diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index f154ba4cdc..6f72fa6e0f 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlBitStringTest < ActiveRecord::TestCase +class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper @@ -14,30 +13,34 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase @connection.create_table('postgresql_bit_strings', :force => true) do |t| t.bit :a_bit, default: "00000011", limit: 8 t.bit_varying :a_bit_varying, default: "0011", limit: 4 + t.bit :another_bit + t.bit_varying :another_bit_varying end end def teardown return unless @connection - @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings' + @connection.drop_table 'postgresql_bit_strings', if_exists: true end def test_bit_string_column column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlBitString.type_for_attribute("a_bit") + assert_not type.binary? end def test_bit_string_varying_column column = PostgresqlBitString.columns_hash["a_bit_varying"] assert_equal :bit_varying, column.type assert_equal "bit varying(4)", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlBitString.type_for_attribute("a_bit_varying") + assert_not type.binary? end def test_default diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index aeebec034d..b6bb1929e6 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,7 +1,6 @@ -# encoding: utf-8 require "cases/helper" -class PostgresqlByteaTest < ActiveRecord::TestCase +class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase class ByteaDataType < ActiveRecord::Base self.table_name = 'bytea_data_type' end @@ -17,10 +16,11 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end end @column = ByteaDataType.columns_hash['payload'] + @type = ByteaDataType.type_for_attribute("payload") end teardown do - @connection.execute 'drop table if exists bytea_data_type' + @connection.drop_table 'bytea_data_type', if_exists: true end def test_column @@ -40,16 +40,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase data = "\u001F\x8B" assert_equal('UTF-8', data.encoding.name) - assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name) + assert_equal('ASCII-8BIT', @type.deserialize(data).encoding.name) end def test_type_cast_binary_value data = "\u001F\x8B".force_encoding("BINARY") - assert_equal(data, @column.type_cast_from_database(data)) + assert_equal(data, @type.deserialize(data)) end def test_type_case_nil - assert_equal(nil, @column.type_cast_from_database(nil)) + assert_equal(nil, @type.deserialize(nil)) end def test_read_value diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index 6c1b29f7fe..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 @@ -26,6 +26,13 @@ module ActiveRecord connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type end + + def test_change_type_with_array + connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp + column = connection.columns(:strings).find { |c| c.name == 'somedate' } + assert_equal :datetime, column.type + assert column.array? + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 54b679d3ab..52f2a0096c 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -3,21 +3,21 @@ 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") ip2 = IPAddr.new("127.0.0.1") - assert_equal "255.0.0.0/8", type.type_cast_for_database(ip) - assert_equal "127.0.0.1/32", type.type_cast_for_database(ip2) + assert_equal "255.0.0.0/8", type.serialize(ip) + assert_equal "127.0.0.1/32", type.serialize(ip2) end test "casting does nothing with non-IPAddr objects" do type = OID::Cidr.new - assert_equal "foo", type.type_cast_for_database("foo") + assert_equal "foo", type.serialize("foo") end end end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 5a8083f7a7..bd62041e79 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,9 +1,8 @@ -# encoding: utf-8 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' @@ -20,7 +19,7 @@ if ActiveRecord::Base.connection.supports_extensions? end teardown do - @connection.execute 'DROP TABLE IF EXISTS citexts;' + @connection.drop_table 'citexts', if_exists: true disable_extension!('citext', @connection) end @@ -32,9 +31,10 @@ if ActiveRecord::Base.connection.supports_extensions? column = Citext.columns_hash['cival'] assert_equal :citext, column.type assert_equal 'citext', column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = Citext.type_for_attribute('cival') + assert_not type.binary? end def test_change_table_supports_json @@ -72,7 +72,7 @@ if ActiveRecord::Base.connection.supports_extensions? def test_schema_dump_with_shorthand output = dump_table_schema("citexts") - assert_match %r[t.citext "cival"], output + assert_match %r[t\.citext "cival"], output end end end diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..8470329c35 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :postgresql_collations, force: true do |t| + t.string :string_c, collation: 'C' + t.text :text_posix, collation: 'POSIX' + end + end + + def teardown + @connection.drop_table :postgresql_collations, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "text column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "add column with collation" do + @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "change column with collation" do + @connection.add_column :postgresql_collations, :description, :string + @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("postgresql_collations") + assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 24c1969dee..1de87e5f01 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' @@ -30,7 +29,7 @@ module PostgresqlCompositeBehavior def teardown super - @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' + @connection.drop_table 'postgresql_composites', if_exists: true @connection.execute 'DROP TYPE IF EXISTS full_address' reset_connection PostgresqlComposite.reset_column_information @@ -41,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 @@ -50,9 +49,10 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase column = PostgresqlComposite.columns_hash["address"] assert_nil column.type assert_equal "full_address", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlComposite.type_for_attribute("address") + assert_not type.binary? end def test_composite_mapping @@ -77,23 +77,23 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end -class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior class FullAddressType < ActiveRecord::Type::Value def type; :full_address end - def type_cast_from_database(value) + def deserialize(value) if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ FullAddress.new($1, $2) end end - def type_cast_from_user(value) + def cast(value) value end - def type_cast_for_database(value) + def serialize(value) return if value.nil? "(#{value.city},#{value.street})" end @@ -111,9 +111,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase column = PostgresqlComposite.columns_hash["address"] assert_equal :full_address, column.type assert_equal "full_address", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlComposite.type_for_attribute("address") + assert_not type.binary? end def test_composite_mapping diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index ab7fd3c6d5..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 @@ -126,12 +126,12 @@ module ActiveRecord end def test_statement_key_is_logged - bindval = 1 - @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]]) + bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) + @connection.exec_query('SELECT $1::integer', 'SQL', [bind]) name = @subscriber.payloads.last[:statement_name] assert name - res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})") - plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first + res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") + plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first assert_operator plan.length, :>, 0 end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 4f48a7bce3..232c25cb3b 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -11,8 +11,8 @@ end class PostgresqlLtree < ActiveRecord::Base end -class PostgresqlDataTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false def setup @connection = ActiveRecord::Base.connection @@ -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 ebb04814bb..6102ddacd1 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -class PostgresqlDomainTest < ActiveRecord::TestCase +class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlDomain < ActiveRecord::Base @@ -20,7 +19,7 @@ class PostgresqlDomainTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_domains' + @connection.drop_table 'postgresql_domains', if_exists: true @connection.execute 'DROP DOMAIN IF EXISTS custom_money' reset_connection end @@ -29,9 +28,10 @@ class PostgresqlDomainTest < ActiveRecord::TestCase column = PostgresqlDomain.columns_hash["price"] assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type - assert column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlDomain.type_for_attribute("price") + assert_not type.binary? end def test_domain_acts_like_basetype diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 88b3b2cc0e..6816a6514b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -class PostgresqlEnumTest < ActiveRecord::TestCase +class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlEnum < ActiveRecord::Base @@ -22,7 +21,7 @@ class PostgresqlEnumTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_enums' + @connection.drop_table 'postgresql_enums', if_exists: true @connection.execute 'DROP TYPE IF EXISTS mood' reset_connection end @@ -31,9 +30,10 @@ class PostgresqlEnumTest < ActiveRecord::TestCase column = PostgresqlEnum.columns_hash["current_mood"] assert_equal :enum, column.type assert_equal "mood", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlEnum.type_for_attribute("current_mood") + assert_not type.binary? end def test_enum_defaults @@ -80,4 +80,12 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal "happy", enum.current_mood end + + def test_assigning_enum_to_nil + model = PostgresqlEnum.new(current_mood: nil) + + assert_nil model.current_mood + assert model.save + assert_nil model.reload.current_mood + end end 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 7b99fcdda0..9cfc133308 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,7 +1,7 @@ require "cases/helper" -class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false class EnableHstore < ActiveRecord::Migration def change diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index a370a5adc6..bde7513339 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,8 +1,7 @@ -# encoding: utf-8 require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlFullTextTest < ActiveRecord::TestCase +class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Tsvector < ActiveRecord::Base; end @@ -14,16 +13,17 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS tsvectors;' + @connection.drop_table 'tsvectors', if_exists: true end def test_tsvector_column column = Tsvector.columns_hash["text_vector"] assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = Tsvector.type_for_attribute("text_vector") + assert_not type.binary? end def test_update_tsvector @@ -39,6 +39,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema("tsvectors") - assert_match %r{t.tsvector "text_vector"}, output + assert_match %r{t\.tsvector "text_vector"}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index ed2bf554bb..0baf985654 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -1,13 +1,20 @@ -# -*- coding: utf-8 -*- 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 @@ -15,28 +22,45 @@ 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.execute 'DROP TABLE IF EXISTS postgresql_points' + @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 column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlPoint.type_for_attribute("x") + assert_not type.binary? 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 @@ -84,7 +181,7 @@ class PostgresqlGeometricTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_geometrics' + @connection.drop_table 'postgresql_geometrics', if_exists: true end def test_geometric_types diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index a0aa10630c..6a2d501646 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,9 +1,8 @@ -# encoding: utf-8 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' @@ -28,11 +27,13 @@ if ActiveRecord::Base.connection.supports_extensions? t.hstore 'settings' end end + Hstore.reset_column_information @column = Hstore.columns_hash['tags'] + @type = Hstore.type_for_attribute("tags") end teardown do - @connection.execute 'drop table if exists hstores' + @connection.drop_table 'hstores', if_exists: true end def test_hstore_included_in_extensions @@ -54,9 +55,9 @@ if ActiveRecord::Base.connection.supports_extensions? def test_column assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type - assert_not @column.number? - assert_not @column.binary? assert_not @column.array? + + assert_not @type.binary? end def test_default @@ -110,10 +111,10 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_type_cast_hstore - assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) - assert_equal({}, @column.type_cast_from_database("")) - assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({'1' => '2'}, @type.deserialize("\"1\"=>\"2\"")) + assert_equal({}, @type.deserialize("")) + assert_equal({'key'=>nil}, @type.deserialize('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -165,47 +166,47 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_gen1 - assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) + assert_equal(%q(" "=>""), @type.serialize({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) + assert_equal(%q(","=>""), @type.serialize({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) + assert_equal(%q("="=>""), @type.serialize({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) + assert_equal(%q(">"=>""), @type.serialize({'>'=>''})) end def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ ")) + assert_equal({" " => " "}, @type.deserialize("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>")) + assert_equal({"=" => ">"}, @type.deserialize("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w')) + assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w')) + assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w')) + assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w')) + assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w')) end def test_rewrite @@ -317,7 +318,7 @@ if ActiveRecord::Base.connection.supports_extensions? def test_schema_dump_with_shorthand output = dump_table_schema("hstores") - assert_match %r[t.hstore "tags",\s+default: {}], output + assert_match %r[t\.hstore "tags",\s+default: {}], output end private diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 74163ac712..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 @@ -15,7 +15,7 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase end teardown do - @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + @connection.drop_table 'postgresql_infinities', if_exists: true end test "type casting infinity on a float column" do @@ -24,6 +24,15 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase assert_equal Float::INFINITY, record.float end + test "type casting string on a float column" do + record = PostgresqlInfinity.new(float: 'Infinity') + assert_equal Float::INFINITY, record.float + record = PostgresqlInfinity.new(float: '-Infinity') + assert_equal(-Float::INFINITY, record.float) + record = PostgresqlInfinity.new(float: 'NaN') + assert_send [record.float, :nan?] + end + test "update_all with infinity on a float column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(float: Float::INFINITY) diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index 7f8751281e..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 @@ -16,7 +16,7 @@ class PostgresqlIntegerTest < ActiveRecord::TestCase end teardown do - @connection.execute "drop table if exists pg_integers" + @connection.drop_table "pg_integers", if_exists: true end test "schema properly respects bigint ranges" do diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 7be7e00463..f242f32496 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# -*- coding: utf-8 -*- require "cases/helper" require 'support/schema_dumping_helper' @@ -14,29 +14,28 @@ module PostgresqlJSONSharedTestCases def setup @connection = ActiveRecord::Base.connection begin - @connection.transaction do - @connection.create_table('json_data_type') do |t| - t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} - t.public_send column_type, 'settings' # t.json 'settings' - end + @connection.create_table('json_data_type') do |t| + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end rescue ActiveRecord::StatementInvalid - skip "do not test on PG without json" + skip "do not test on PostgreSQL without #{column_type} type." end - @column = JsonDataType.columns_hash['payload'] end def teardown - @connection.execute 'drop table if exists json_data_type' + @connection.drop_table :json_data_type, if_exists: true + JsonDataType.reset_column_information end def test_column column = JsonDataType.columns_hash["payload"] assert_equal column_type, column.type assert_equal column_type.to_s, column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? end def test_default @@ -66,7 +65,7 @@ module PostgresqlJSONSharedTestCases def test_schema_dumping output = dump_table_schema("json_data_type") - assert_match(/t.#{column_type.to_s}\s+"payload",\s+default: {}/, output) + assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) end def test_cast_value_on_write @@ -78,16 +77,16 @@ module PostgresqlJSONSharedTestCases end def test_type_cast_json - column = JsonDataType.columns_hash["payload"] + type = JsonDataType.type_for_attribute("payload") data = "{\"a_key\":\"a_value\"}" - hash = column.type_cast_from_database(data) + hash = type.deserialize(data) assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) + assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) - assert_equal({}, column.type_cast_from_database("{}")) - assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"}))) + assert_equal({}, type.deserialize("{}")) + assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) end def test_rewrite @@ -179,9 +178,17 @@ module PostgresqlJSONSharedTestCases assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) assert_not json.changed? end + + def test_assigning_invalid_json + json = JsonDataType.new + + json.payload = 'foo' + + assert_nil json.payload + end end -class PostgresqlJSONTest < ActiveRecord::TestCase +class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type @@ -189,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 771a825840..56516c82b4 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,8 +1,7 @@ -# encoding: utf-8 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' @@ -23,16 +22,17 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists ltrees' + @connection.drop_table 'ltrees', if_exists: true end def test_column column = Ltree.columns_hash['path'] assert_equal :ltree, column.type assert_equal "ltree", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = Ltree.type_for_attribute('path') + assert_not type.binary? end def test_write @@ -48,6 +48,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema("ltrees") - assert_match %r[t.ltree "path"], output + assert_match %r[t\.ltree "path"], output end end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index f3a24eee85..e3670c203d 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,8 +1,7 @@ -# encoding: utf-8 require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlMoneyTest < ActiveRecord::TestCase +class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlMoney < ActiveRecord::Base; end @@ -11,13 +10,13 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") @connection.create_table('postgresql_moneys', force: true) do |t| - t.column "wealth", "money" - t.column "depth", "money", default: "150.55" + t.money "wealth" + t.money "depth", default: "150.55" end end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys' + @connection.drop_table 'postgresql_moneys', if_exists: true end def test_column @@ -25,9 +24,10 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_equal :money, column.type assert_equal "money", column.sql_type assert_equal 2, column.scale - assert column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlMoney.type_for_attribute("wealth") + assert_not type.binary? end def test_default @@ -46,17 +46,17 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase end def test_money_type_cast - column = PostgresqlMoney.columns_hash['wealth'] - assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12")) - assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12")) - assert_equal(-1.15, column.type_cast_from_user("-$1.15")) - assert_equal(-2.25, column.type_cast_from_user("($2.25)")) + type = PostgresqlMoney.type_for_attribute('wealth') + assert_equal(12345678.12, type.cast("$12,345,678.12")) + assert_equal(12345678.12, type.cast("$12.345.678,12")) + assert_equal(-1.15, type.cast("-$1.15")) + assert_equal(-2.25, type.cast("($2.25)")) end def test_schema_dumping 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 daa590f369..fe6ee4e2d9 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,8 +1,7 @@ -# encoding: utf-8 require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlNetworkTest < ActiveRecord::TestCase +class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlNetworkAddress < ActiveRecord::Base; end @@ -16,34 +15,37 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_network_addresses' + @connection.drop_table 'postgresql_network_addresses', if_exists: true end def test_cidr_column column = PostgresqlNetworkAddress.columns_hash["cidr_address"] assert_equal :cidr, column.type assert_equal "cidr", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlNetworkAddress.type_for_attribute("cidr_address") + assert_not type.binary? end def test_inet_column column = PostgresqlNetworkAddress.columns_hash["inet_address"] assert_equal :inet, column.type assert_equal "inet", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlNetworkAddress.type_for_attribute("inet_address") + assert_not type.binary? end def test_macaddr_column column = PostgresqlNetworkAddress.columns_hash["mac_address"] assert_equal :macaddr, column.type assert_equal "macaddr", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = PostgresqlNetworkAddress.type_for_attribute("mac_address") + assert_not type.binary? end def test_network_types @@ -85,8 +87,8 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema("postgresql_network_addresses") - assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output - assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output - assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output + assert_match %r{t\.inet\s+"inet_address",\s+default: "192\.168\.1\.1"}, output + assert_match %r{t\.cidr\s+"cidr_address",\s+default: "192\.168\.1\.0/24"}, output + assert_match %r{t\.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 70aa898439..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 @@ -12,7 +12,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_numbers' + @connection.drop_table 'postgresql_numbers', if_exists: true end def test_data_type @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_same ::Float::NAN, third.double + assert_send [third.double, :nan?] end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 6bb2b26cd5..6e6850c4a9 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,11 +1,10 @@ -# encoding: utf-8 require "cases/helper" require 'support/ddl_helper' require 'support/connection_helper' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapterTest < ActiveRecord::TestCase + class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase include DdlHelper include ConnectionHelper @@ -69,7 +68,7 @@ module ActiveRecord def test_insert_sql_with_proprietary_returning_clause with_example_table do id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id + assert_equal 5150, id end end @@ -107,21 +106,21 @@ module ActiveRecord connection = connection_without_insert_returning id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, id + assert_equal expect.to_i, id end def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_exec_insert_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_sql_for_insert_with_returning_disabled @@ -228,8 +227,8 @@ module ActiveRecord "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" ) ensure - @connection.exec_query('DROP TABLE IF EXISTS ex') - @connection.exec_query('DROP TABLE IF EXISTS ex2') + @connection.drop_table 'ex', if_exists: true + @connection.drop_table 'ex2', if_exists: true end def test_exec_insert_number @@ -239,7 +238,7 @@ module ActiveRecord result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last + assert_equal 10, result.rows.last.last end end @@ -275,7 +274,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -284,12 +283,12 @@ module ActiveRecord string = @connection.quote('foo') @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]]) + 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -298,14 +297,14 @@ module ActiveRecord string = @connection.quote('foo') @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - column = @connection.columns('ex').find { |col| col.name == 'id' } + bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']]) + 'SELECT id, data FROM ex WHERE id = $1', nil, [bind]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -437,10 +436,10 @@ module ActiveRecord private def insert(ctx, data) - binds = data.map { |name, value| - [ctx.columns('ex').find { |x| x.name == name }, value] + binds = data.map { |name, value| + bind_param(value, name) } - columns = binds.map(&:first).map(&:name) + columns = binds.map(&:name) bind_subs = columns.length.times.map { |x| "$#{x + 1}" } @@ -457,6 +456,10 @@ module ActiveRecord def connection_without_insert_returning ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) end + + def bind_param(value, name = nil) + ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 9ac0036d66..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 @@ -27,20 +27,16 @@ module ActiveRecord assert_equal "'Infinity'", @conn.quote(infinity) end - def test_quote_time_usec - assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) - assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) - end - def test_quote_range range = "1,2]'; SELECT * FROM users; --".."a" - c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) - assert_equal "'[1,0]'", @conn.quote(range, c) + type = OID::Range.new(Type::Integer.new, :int8range) + assert_equal "'[1,0]'", @conn.quote(type.serialize(range)) end def test_quote_bit_string - c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) - assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + value = "'); SELECT * FROM users; /*\n01\n*/--" + type = OID::Bit.new + assert_equal nil, @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 70cf21100a..02b1083430 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,13 +1,13 @@ 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 - self.use_transactional_fixtures = false + class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false include ConnectionHelper def setup @@ -91,7 +91,7 @@ _SQL end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' + @connection.drop_table 'postgresql_ranges', if_exists: true @connection.execute 'DROP TYPE IF EXISTS floatrange' reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb new file mode 100644 index 0000000000..c895ab9db5 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -0,0 +1,111 @@ +require 'cases/helper' +require 'support/connection_helper' + +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + include ConnectionHelper + + IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| + sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + end + + module MissingSuperuserPrivileges + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + super "BROKEN;" rescue nil # put transaction in broken state + raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege' + else + super + end + end + end + + module ProgrammerMistake + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + raise ArgumentError, 'something is not right.' + else + super + end + end + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + reset_connection + if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges) + raise "MissingSuperuserPrivileges patch was not removed" + end + end + + def test_should_reraise_invalid_foreign_key_exception_and_show_warning + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::InvalidForeignKey) do + @connection.disable_referential_integrity do + raise ActiveRecord::InvalidForeignKey, 'Should be re-raised' + end + end + assert_equal 'Should be re-raised', e.message + end + assert_match (/WARNING: Rails was not able to disable referential integrity/), warning + assert_match (/cause: PG::InsufficientPrivilege/), warning + end + + def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::StatementInvalid) do + @connection.disable_referential_integrity do + raise ActiveRecord::StatementInvalid, 'Should be re-raised' + end + end + assert_equal 'Should be re-raised', e.message + end + assert warning.blank?, "expected no warnings but got:\n#{warning}" + end + + def test_does_not_break_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + assert_transaction_is_not_broken + end + end + + def test_does_not_break_nested_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.transaction(requires_new: true) do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + end + assert_transaction_is_not_broken + end + end + + def test_only_catch_active_record_errors_others_bubble_up + @connection.extend ProgrammerMistake + + assert_raises ArgumentError do + @connection.disable_referential_integrity {} + end + end + + private + + def assert_transaction_is_not_broken + assert_equal 1, @connection.select_value("SELECT 1") + end +end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index 056a035622..bd64bae308 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -1,14 +1,14 @@ require "cases/helper" -class PostgresqlRenameTableTest < ActiveRecord::TestCase +class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.create_table :before_rename, force: true end def teardown - @connection.execute 'DROP TABLE IF EXISTS "before_rename"' - @connection.execute 'DROP TABLE IF EXISTS "after_rename"' + @connection.drop_table "before_rename", if_exists: true + @connection.drop_table "after_rename", if_exists: true end test "renaming a table also renames the primary key index" do diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 99c26c4bf7..fa6584eae5 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -3,8 +3,8 @@ require "cases/helper" class SchemaThing < ActiveRecord::Base end -class SchemaAuthorizationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false TABLE_NAME = 'schema_things' COLUMNS = [ @@ -55,7 +55,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase set_session_auth USERS.each do |u| set_session_auth u - assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name'] set_session_auth end end @@ -67,7 +67,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase USERS.each do |u| @connection.clear_cache! set_session_auth u - assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name'] set_session_auth end end @@ -111,4 +111,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase @connection.session_auth = auth || 'default' end + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index e99f1e2867..9aba2b5976 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -2,8 +2,8 @@ require "cases/helper" require 'models/default' require 'support/schema_dumping_helper' -class SchemaTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class SchemaTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' SCHEMA2_NAME = 'test_schema2' @@ -151,10 +151,10 @@ class SchemaTest < ActiveRecord::TestCase def test_schema_change_with_prepared_stmt altered = false - @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] + @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] @connection.exec_query "alter table developers add column zomg int", 'sql', [] altered = true - @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] + @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @@ -384,16 +384,16 @@ class SchemaTest < ActiveRecord::TestCase def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") - assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end @@ -435,9 +435,13 @@ class SchemaTest < ActiveRecord::TestCase assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end + + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end end -class SchemaForeignKeyTest < ActiveRecord::TestCase +class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do @@ -454,15 +458,15 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase end @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id" output = dump_table_schema "wagons" - assert_match %r{\s+add_foreign_key "wagons", "my_schema.trains", column: "train_id"$}, output + assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output ensure - @connection.execute "DROP TABLE IF EXISTS wagons" - @connection.execute "DROP TABLE IF EXISTS my_schema.trains" + @connection.drop_table "wagons", if_exists: true + @connection.drop_table "my_schema.trains", if_exists: true @connection.execute "DROP SCHEMA IF EXISTS my_schema" 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" diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb new file mode 100644 index 0000000000..7d30db247b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -0,0 +1,60 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlSerial < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "postgresql_serials", force: true do |t| + t.serial :seq + end + end + + teardown do + @connection.drop_table "postgresql_serials", if_exists: true + end + + def test_serial_column + column = PostgresqlSerial.columns_hash["seq"] + assert_equal :integer, column.type + assert_equal "integer", column.sql_type + assert column.serial? + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "postgresql_serials" + assert_match %r{t\.serial\s+"seq"}, output + end +end + +class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlBigSerial < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "postgresql_big_serials", force: true do |t| + t.bigserial :seq + end + end + + teardown do + @connection.drop_table "postgresql_big_serials", if_exists: true + end + + def test_bigserial_column + column = PostgresqlBigSerial.columns_hash["seq"] + assert_equal :integer, column.type + assert_equal "bigint", column.sql_type + assert column.serial? + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "postgresql_big_serials" + assert_match %r{t\.bigserial\s+"seq"}, output + end +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 eb32c4d2c2..4c4866b46b 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,10 +2,10 @@ 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_fixtures = false + self.use_transactional_tests = false setup do @connection = ActiveRecord::Base.connection @@ -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 @@ -70,53 +70,6 @@ class TimestampTest < ActiveRecord::TestCase assert_equal(-1.0 / 0.0, d.updated_at) end - def test_default_datetime_precision - ActiveRecord::Base.connection.create_table(:foos) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime - assert_nil activerecord_column_option('foos', 'created_at', 'precision') - end - - def test_timestamp_data_type_with_precision - ActiveRecord::Base.connection.create_table(:foos) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, :precision => 0 - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, :precision => 5 - assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_timestamps_helper_with_custom_precision - ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :null => true, :precision => 4 - end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') - end - - def test_passing_precision_to_timestamp_does_not_set_limit - ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :null => true, :precision => 4 - end - assert_nil activerecord_column_option("foos", "created_at", "limit") - assert_nil activerecord_column_option("foos", "updated_at", "limit") - end - - def test_invalid_timestamp_precision_raises_error - assert_raises ActiveRecord::ActiveRecordError do - ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :null => true, :precision => 7 - end - end - end - - def test_postgres_agrees_with_activerecord_about_precision - ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :null => true, :precision => 4 - end - assert_equal '4', pg_datetime_precision('foos', 'created_at') - assert_equal '4', pg_datetime_precision('foos', 'updated_at') - end - def test_bc_timestamp date = Date.new(0) - 1.week Developer.create!(:name => "aaron", :updated_at => date) @@ -134,21 +87,4 @@ class TimestampTest < ActiveRecord::TestCase Developer.create!(:name => "yahagi", :updated_at => date) assert_equal date, Developer.find_by_name("yahagi").updated_at end - - private - - def pg_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] - end - - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index c88259d274..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 @@ -18,8 +18,8 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") big_array = [123456789123456789] - assert_raises(RangeError) { int_array.type_cast_from_user(big_array) } - assert_equal big_array, bigint_array.type_cast_from_user(big_array) + assert_raises(RangeError) { int_array.serialize(big_array) } + assert_equal "{123456789123456789}", bigint_array.serialize(big_array) end test "range types correctly respect registration of subtypes" do @@ -27,7 +27,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_range = @connection.type_map.lookup(3926, -1, "int8range") big_range = 0..123456789123456789 - assert_raises(RangeError) { int_range.type_cast_for_database(big_range) } - assert_equal "[0,123456789123456789]", bigint_range.type_cast_for_database(big_range) + assert_raises(RangeError) { int_range.serialize(big_range) } + assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) end 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 7d2fae69d5..7127d69e9e 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'support/schema_dumping_helper' @@ -8,11 +7,11 @@ module PostgresqlUUIDHelper end def drop_table(name) - connection.execute "drop table if exists #{name}" + connection.drop_table name, if_exists: true end end -class PostgresqlUUIDTest < ActiveRecord::TestCase +class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -49,9 +48,10 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type assert_equal "uuid", column.sql_type - assert_not column.number? - assert_not column.binary? assert_not column.array? + + type = UUIDType.type_for_attribute("guid") + assert_not type.binary? end def test_treat_blank_uuid_as_nil @@ -114,7 +114,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema "uuid_data_type" - assert_match %r{t.uuid "guid"}, output + assert_match %r{t\.uuid "guid"}, output end def test_uniqueness_validation_ignores_uuid @@ -135,27 +135,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end end -class PostgresqlLargeKeysTest < ActiveRecord::TestCase - include PostgresqlUUIDHelper - include SchemaDumpingHelper - - def setup - connection.create_table('big_serials', id: :bigserial) do |t| - t.string 'name' - end - end - - def test_omg - schema = dump_table_schema "big_serials" - assert_match "create_table \"big_serials\", id: :bigserial", schema - end - - def teardown - drop_table "big_serials" - end -end - -class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase +class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -230,7 +210,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end end -class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase +class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -264,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 5aba118518..add32699fa 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,8 +1,7 @@ -# encoding: utf-8 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' @@ -23,7 +22,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists xml_data_type' + @connection.drop_table 'xml_data_type', if_exists: true end def test_column @@ -50,6 +49,6 @@ class PostgresqlXMLTest < ActiveRecord::TestCase def test_schema_dump_with_shorthand output = dump_table_schema("xml_data_type") - assert_match %r{t.xml "payload"}, output + assert_match %r{t\.xml "payload"}, output end end diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb new file mode 100644 index 0000000000..58a9469ce5 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class SQLite3CollationTest < ActiveRecord::SQLite3TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :collation_table_sqlite3, force: true do |t| + t.string :string_nocase, collation: 'NOCASE' + t.text :text_rtrim, collation: 'RTRIM' + end + end + + def teardown + @connection.drop_table :collation_table_sqlite3, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' } + assert_equal :string, column.type + assert_equal 'NOCASE', column.collation + end + + test "text column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "add column with collation" do + @connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'RTRIM', column.collation + end + + test "change column with collation" do + @connection.add_column :collation_table_sqlite3, :description, :string + @connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("collation_table_sqlite3") + assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 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 df497e761c..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', @@ -85,9 +85,9 @@ module ActiveRecord def test_quoting_binary_strings value = "hello".encode('ascii-8bit') - column = Column.new(nil, 1, SQLite3String.new) + type = Type::String.new - assert_equal "'hello'", @conn.quote(value, column) + assert_equal "'hello'", @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 029663e7f4..7996e7ad50 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'models/owner' require 'tempfile' @@ -6,10 +5,10 @@ require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class SQLite3AdapterTest < ActiveRecord::TestCase + class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase include DdlHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class DualEncoding < ActiveRecord::Base end @@ -23,7 +22,7 @@ module ActiveRecord def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db") - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end @@ -83,8 +82,7 @@ module ActiveRecord def test_exec_insert with_example_table do - column = @conn.columns('ex').find { |col| col.name == 'number' } - vals = [[column, 10]] + vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)] @conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals) result = @conn.exec_query( @@ -157,7 +155,7 @@ module ActiveRecord with_example_table 'id int, data string' do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -169,10 +167,9 @@ module ActiveRecord def test_exec_query_typecasts_bind_vals with_example_table 'id int, data string' do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - column = @conn.columns('ex').find { |col| col.name == 'id' } result = @conn.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -194,7 +191,7 @@ module ActiveRecord binary.save! assert_equal str, binary.data ensure - DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings') + DualEncoding.connection.drop_table 'dual_encodings', if_exists: true end def test_type_cast_should_not_mutate_encoding @@ -424,14 +421,14 @@ module ActiveRecord end def test_statement_closed - db = SQLite3::Database.new(ActiveRecord::Base. + db = ::SQLite3::Database.new(ActiveRecord::Base. configurations['arunit']['database']) - statement = SQLite3::Statement.new(db, + statement = ::SQLite3::Statement.new(db, 'CREATE TABLE statement_test (number integer not null)') - statement.stubs(:step).raises(SQLite3::BusyException, 'busy') + statement.stubs(:step).raises(::SQLite3::BusyException, 'busy') statement.stubs(:columns).once.returns([]) statement.expects(:close).once - SQLite3::Statement.stubs(:new).returns(statement) + ::SQLite3::Statement.stubs(:new).returns(statement) assert_raises ActiveRecord::StatementInvalid do @conn.exec_query 'select * from statement_test' 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 f545fc2011..887dcfc96c 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -1,10 +1,9 @@ -# encoding: utf-8 require "cases/helper" 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/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index f4e8003bc3..9d5327bf35 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -3,7 +3,7 @@ require "cases/helper" if ActiveRecord::Base.connection.supports_migrations? class ActiveRecordSchemaTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @original_verbose = ActiveRecord::Migration.verbose diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index 3e0032ec73..472e270f8c 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -8,12 +8,7 @@ module ActiveRecord test 'does not duplicate conditions' do scope = AssociationScope.scope(Author.new.association(:welcome_posts), Author.connection) - wheres = scope.where_values.map(&:right) - binds = scope.bind_values.map(&:last) - wheres = scope.where_values.map(&:right).reject { |node| - Arel::Nodes::BindParam === node - } - assert_equal wheres.uniq, wheres + binds = scope.where_clause.binds.map(&:value) assert_equal binds.uniq, binds 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 17394cb6f7..ba90c61d65 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -58,6 +58,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end end + def test_optional_relation + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company, optional: true + end + + account = model.new + assert account.valid? + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + + def test_not_optional_relation + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company, optional: false + end + + account = model.new + refute account.valid? + assert_equal [{error: :blank}], account.errors.details[:company] + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + + def test_required_belongs_to_config + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company + end + + account = model.new + refute account.valid? + assert_equal [{error: :blank}], account.errors.details[:company] + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + def test_default_scope_on_relations_is_not_cached counter = 0 @@ -74,9 +124,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase where("id = :inc", :inc => counter) } - has_many :comments, :class => comments + has_many :comments, :anonymous_class => comments } - belongs_to :post, :class => posts, :inverse_of => false + belongs_to :post, :anonymous_class => posts, :inverse_of => false } assert_equal 0, counter @@ -122,14 +172,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first - assert citibank_result.association_cache.key?(:firm_with_primary_key) + assert citibank_result.association(:firm_with_primary_key).loaded? end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first - assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols) + assert citibank_result.association(:firm_with_primary_key_symbols).loaded? end def test_creating_the_belonging_object @@ -372,6 +422,24 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 371635d20a..ffbf60e390 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -77,9 +77,17 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_has_many_through_with_order authors = Author.includes(:favorite_authors).to_a + assert authors.count > 0 assert_no_queries { authors.map(&:favorite_authors) } end + def test_eager_loaded_has_one_association_with_references_does_not_run_additional_queries + Post.update_all(author_id: nil) + authors = Author.includes(:post).references(:post).to_a + assert authors.count > 0 + assert_no_queries { authors.map(&:post) } + end + def test_with_two_tables_in_from_without_getting_double_quoted posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a assert_equal 2, posts.first.comments.size @@ -751,6 +759,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -1142,7 +1167,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert client.accounts.empty? } end - def test_preloading_has_many_through_with_uniq + def test_preloading_has_many_through_with_distinct mary = Author.includes(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length 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 aea9207bfe..e584c94ad8 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 @@ -83,6 +83,16 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +class DeveloperWithExtendOption < Developer + module NamedExtension + def category + 'sns' + end + end + + has_and_belongs_to_many :projects, extend: NamedExtension +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -234,7 +244,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers, new_project.developers end - def test_habtm_unique_order_preserved + def test_habtm_distinct_order_preserved assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers end @@ -339,7 +349,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 'Yet Another Testing Title', another_post.title end - def test_uniq_after_the_fact + def test_distinct_after_the_fact dev = developers(:jamis) dev.projects << projects(:active_record) dev.projects << projects(:active_record) @@ -348,13 +358,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, dev.projects.distinct.size end - def test_uniq_before_the_fact + def test_distinct_before_the_fact projects(:active_record).developers << developers(:jamis) projects(:active_record).developers << developers(:david) assert_equal 3, projects(:active_record, :reload).developers.size end - def test_uniq_option_prevents_duplicate_push + def test_distinct_option_prevents_duplicate_push project = projects(:active_record) project.developers << developers(:jamis) project.developers << developers(:david) @@ -365,7 +375,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end - def test_uniq_when_association_already_loaded + def test_distinct_when_association_already_loaded project = projects(:active_record) project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] assert_equal 3, Project.includes(:developers).find(project.id).developers.size @@ -577,6 +587,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first end + def test_association_with_extend_option + eponine = DeveloperWithExtendOption.create(name: 'Eponine') + assert_equal 'sns', eponine.projects.category + end + def test_replace_with_less david = developers(:david) david.projects = [projects(:action_controller)] diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 21a45042fa..2c4e2a875c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -30,9 +30,12 @@ require 'models/college' require 'models/student' require 'models/pirate' require 'models/ship' +require 'models/ship_part' require 'models/tyre' require 'models/subscriber' require 'models/subscription' +require 'models/zine' +require 'models/interest' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -97,7 +100,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :posts, :readers, :taggings, :cars, :jobs, :tags, - :categorizations + :categorizations, :zines, :interests def setup Client.destroyed_client_ids.clear @@ -116,9 +119,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase developer_project = Class.new(ActiveRecord::Base) { self.table_name = 'developers_projects' - belongs_to :developer, :class => dev + belongs_to :developer, :anonymous_class => dev } - has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id' } dev = developer.first named = Developer.find(dev.id) @@ -137,13 +140,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comments = Class.new(ActiveRecord::Base) { self.table_name = 'comments' self.inheritance_column = 'not_there' - belongs_to :post, :class => post + belongs_to :post, :anonymous_class => post default_scope -> { counter += 1 where("id = :inc", :inc => counter) } } - has_many :comments, :class => comments, :foreign_key => 'post_id' + has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id' } assert_equal 0, counter post = posts.first @@ -160,6 +163,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal college.students, Student.where(active: true, college_id: college.id) end + def test_add_record_to_collection_should_change_its_updated_at + ship = Ship.create(name: 'dauntless') + part = ShipPart.create(name: 'cockpit') + updated_at = part.updated_at + + ship.parts << part + + assert_equal part.ship, ship + assert_not_equal part.updated_at, updated_at + end + + def test_clear_collection_should_not_change_updated_at + # GH#17161: .clear calls delete_all (and returns the association), + # which is intended to not touch associated objects's updated_at field + ship = Ship.create(name: 'dauntless') + part = ShipPart.create(name: 'cockpit', ship_id: ship.id) + + ship.parts.clear + part.reload + + assert_equal nil, part.ship + assert !part.updated_at_changed? + end + def test_create_from_association_should_respect_default_scope car = Car.create(:name => 'honda') assert_equal 'honda', car.name @@ -316,16 +343,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope car = cars(:honda) - scoped_count = car.foo_bulbs.where_values.count + scope = car.foo_bulbs.where_values_hash bulb = car.foo_bulbs.build - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash bulb = car.foo_bulbs.create - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash bulb = car.foo_bulbs.create! - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash end def test_no_sql_should_be_fired_if_association_already_loaded @@ -435,6 +462,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end + def test_taking + posts(:other_by_bob).destroy + assert_equal posts(:misc_by_bob), authors(:bob).posts.take + assert_equal posts(:misc_by_bob), authors(:bob).posts.take! + authors(:bob).posts.to_a + assert_equal posts(:misc_by_bob), authors(:bob).posts.take + assert_equal posts(:misc_by_bob), authors(:bob).posts.take! + end + + def test_taking_not_found + authors(:bob).posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! } + authors(:bob).posts.to_a + assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! } + end + + def test_taking_with_a_number + # taking from unloaded Relation + bob = Author.find(authors(:bob).id) + assert_equal [posts(:misc_by_bob)], bob.posts.take(1) + bob = Author.find(authors(:bob).id) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) + + # taking from loaded Relation + bob.posts.to_a + assert_equal [posts(:misc_by_bob)], authors(:bob).posts.take(1) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], authors(:bob).posts.take(2) + end + + def test_taking_with_inverse_of + interests(:woodsmanship).destroy + interests(:survival).destroy + + zine = zines(:going_out) + interest = zine.interests.take + assert_equal interests(:hunting), interest + assert_same zine, interest.zine + end + def test_cant_save_has_many_readonly_association authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } authors(:david).readonly_comments.each { |c| assert c.readonly? } @@ -466,9 +532,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_update_all_on_association_accessed_before_save firm = Firm.new(name: 'Firm') + clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + assert_not_equal clients_proxy_id, firm.clients.object_id + end + + def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key + # We can use the same cached proxy object because the id is available for the scope + firm = Firm.new(name: 'Firm', id: 100) + clients_proxy_id = firm.clients.object_id + firm.clients << Client.first + firm.save! + assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + assert_equal clients_proxy_id, firm.clients.object_id end def test_belongs_to_sanity @@ -1307,7 +1385,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_nothing_raised { topic.destroy } end - uses_transaction :test_dependence_with_transaction_support_on_failure def test_dependence_with_transaction_support_on_failure firm = companies(:first_firm) clients = firm.clients @@ -1426,6 +1503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted @@ -1679,6 +1758,82 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, firm.clients.size end + def test_calling_none_should_count_instead_of_loading_association + firm = companies(:first_firm) + assert_queries(1) do + firm.clients.none? # use count query + end + assert !firm.clients.loaded? + end + + def test_calling_none_on_loaded_association_should_not_use_query + firm = companies(:first_firm) + firm.clients.collect # force load + assert_no_queries { assert ! firm.clients.none? } + end + + def test_calling_none_should_defer_to_collection_if_using_a_block + firm = companies(:first_firm) + assert_queries(1) do + firm.clients.expects(:size).never + firm.clients.none? { true } + end + assert firm.clients.loaded? + end + + def test_calling_none_should_return_true_if_none + firm = companies(:another_firm) + assert firm.clients_like_ms.none? + assert_equal 0, firm.clients_like_ms.size + end + + def test_calling_none_should_return_false_if_any + firm = companies(:first_firm) + assert !firm.limited_clients.none? + assert_equal 1, firm.limited_clients.size + end + + def test_calling_one_should_count_instead_of_loading_association + firm = companies(:first_firm) + assert_queries(1) do + firm.clients.one? # use count query + end + assert !firm.clients.loaded? + end + + def test_calling_one_on_loaded_association_should_not_use_query + firm = companies(:first_firm) + firm.clients.collect # force load + assert_no_queries { assert ! firm.clients.one? } + end + + def test_calling_one_should_defer_to_collection_if_using_a_block + firm = companies(:first_firm) + assert_queries(1) do + firm.clients.expects(:size).never + firm.clients.one? { true } + end + assert firm.clients.loaded? + end + + def test_calling_one_should_return_false_if_zero + firm = companies(:another_firm) + assert ! firm.clients_like_ms.one? + assert_equal 0, firm.clients_like_ms.size + end + + def test_calling_one_should_return_true_if_one + firm = companies(:first_firm) + assert firm.limited_clients.one? + assert_equal 1, firm.limited_clients.size + end + + def test_calling_one_should_return_false_if_more_than_one + firm = companies(:first_firm) + assert ! firm.clients.one? + assert_equal 3, firm.clients.size + end + def test_joins_with_namespaced_model_should_use_correct_type old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true @@ -1976,11 +2131,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! original_child = FailedBulb.create!(car: car) - assert_raise(ActiveRecord::RecordNotDestroyed) do + error = assert_raise(ActiveRecord::RecordNotDestroyed) do car.failed_bulbs = [FailedBulb.create!] end assert_equal [original_child], car.reload.failed_bulbs + assert_equal "Failed to destroy the record", error.message end test 'updates counter cache when default scope is given' do 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 6729a5a9fc..1d545af5a5 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -84,11 +84,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase subscriber = make_model "Subscriber" subscriber.primary_key = 'nick' - subscription.belongs_to :book, class: book - subscription.belongs_to :subscriber, class: subscriber + subscription.belongs_to :book, anonymous_class: book + subscription.belongs_to :subscriber, anonymous_class: subscriber - book.has_many :subscriptions, class: subscription - book.has_many :subscribers, through: :subscriptions, class: subscriber + book.has_many :subscriptions, anonymous_class: subscription + book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber anonbook = book.first namebook = Book.find anonbook.id @@ -154,10 +154,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lesson_student = make_model 'LessonStudent' lesson_student.table_name = 'lessons_students' - lesson_student.belongs_to :lesson, :class => lesson - lesson_student.belongs_to :student, :class => student - lesson.has_many :lesson_students, :class => lesson_student - lesson.has_many :students, :through => :lesson_students, :class => student + lesson_student.belongs_to :lesson, :anonymous_class => lesson + lesson_student.belongs_to :student, :anonymous_class => student + lesson.has_many :lesson_students, :anonymous_class => lesson_student + lesson.has_many :students, :through => :lesson_students, :anonymous_class => student [lesson, lesson_student, student] end @@ -595,6 +595,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") end + def test_through_record_is_built_when_created_with_where + assert_difference("posts(:thinking).readers.count", 1) do + posts(:thinking).people.where(first_name: "Jeb").create + end + end + def test_associate_with_create_and_no_options peeps = posts(:thinking).people.count posts(:thinking).people.create(:first_name => 'foo') @@ -954,7 +960,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 1, category.categorizations.where(:special => true).count end - def test_joining_has_many_through_with_uniq + def test_joining_has_many_through_with_distinct mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length @@ -1034,14 +1040,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_save_should_not_raise_exception_when_join_record_has_errors - repair_validations(Categorization) do - Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } - c = Category.create(:name => 'Fishing', :authors => [Author.first]) - c.save - end - end - def test_assign_array_to_new_record_builds_join_records c = Category.new(:name => 'Fishing', :authors => [Author.first]) assert_equal 1, c.categorizations.size @@ -1066,11 +1064,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_create_bang_returns_falsy_when_join_record_has_errors + def test_save_returns_falsy_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } c = Category.new(:name => 'Fishing', :authors => [Author.first]) - assert !c.save + assert_not c.save 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 9b6757e256..5c2e5e7b43 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -12,7 +12,7 @@ require 'models/image' require 'models/post' class HasOneAssociationsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates def setup @@ -237,16 +237,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) - scoped_count = pirate.association(:foo_bulb).scope.where_values.count + scope = pirate.association(:foo_bulb).scope.where_values_hash bulb = pirate.build_foo_bulb - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash bulb = pirate.create_foo_bulb - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash bulb = pirate.create_foo_bulb! - assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count + assert_not_equal scope, bulb.scope_after_initialize.where_values_hash end def test_create_association diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 9918601623..5575419c35 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -17,7 +17,7 @@ require 'models/engine' require 'models/car' class AssociationsJoinModelTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, # Reload edges table from fixtures as otherwise repeated test was failing @@ -35,12 +35,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert categories(:sti_test).authors.include?(authors(:mary)) end - def test_has_many_uniq_through_join_model + def test_has_many_distinct_through_join_model assert_equal 2, authors(:mary).categorized_posts.size assert_equal 1, authors(:mary).unique_categorized_posts.size end - def test_has_many_uniq_through_count + def test_has_many_distinct_through_count author = authors(:mary) assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } @@ -49,7 +49,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? end - def test_has_many_uniq_through_find + def test_has_many_distinct_through_find assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size end @@ -625,7 +625,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments end - def test_uniq_has_many_through_should_retain_order + def test_distinct_has_many_through_should_retain_order comment_ids = authors(:david).comments.map(&:id) assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id) assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 321fb6c8dd..3e5494e897 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class RequiredAssociationsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Parent < ActiveRecord::Base end @@ -18,8 +18,8 @@ class RequiredAssociationsTest < ActiveRecord::TestCase end teardown do - @connection.drop_table 'parents' if @connection.table_exists? 'parents' - @connection.drop_table 'children' if @connection.table_exists? 'children' + @connection.drop_table 'parents', if_exists: true + @connection.drop_table 'children', if_exists: true end test "belongs_to associations are not required by default" do @@ -40,7 +40,7 @@ class RequiredAssociationsTest < ActiveRecord::TestCase record = model.new assert_not record.save - assert_equal ["Parent can't be blank"], record.errors.full_messages + assert_equal ["Parent must exist"], record.errors.full_messages record.parent = Parent.new assert record.save @@ -64,12 +64,32 @@ class RequiredAssociationsTest < ActiveRecord::TestCase record = model.new assert_not record.save - assert_equal ["Child can't be blank"], record.errors.full_messages + assert_equal ["Child must exist"], record.errors.full_messages record.child = Child.new assert record.save end + test "required has_one associations have a correct error message" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.create + assert_equal ["Child must exist"], record.errors.full_messages + end + + test "required belongs_to associations have a correct error message" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.create + assert_equal ["Parent must exist"], record.errors.full_messages + end + private def subclass_of(klass, &block) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 72963fd56c..3d202a5527 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -43,28 +43,6 @@ class AssociationsTest < ActiveRecord::TestCase assert_equal favs, fav2 end - def test_clear_association_cache_stored - firm = Firm.find(1) - assert_kind_of Firm, firm - - firm.clear_association_cache - assert_equal Firm.find(1).clients.collect(&:name).sort, firm.clients.collect(&:name).sort - end - - def test_clear_association_cache_new_record - firm = Firm.new - client_stored = Client.find(3) - client_new = Client.new - client_new.name = "The Joneses" - clients = [ client_stored, client_new ] - - firm.clients << clients - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - - firm.clear_association_cache - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - end - def test_loading_the_association_target_should_keep_child_records_marked_for_destruction ship = Ship.create!(:name => "The good ship Dollypop") part = ship.parts.create!(:name => "Mast") @@ -141,7 +119,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_association_with_references firm = companies(:first_firm) - assert_equal ['foo'], firm.association_with_references.references_values + assert_includes firm.association_with_references.references_values, 'foo' end end @@ -238,7 +216,7 @@ class AssociationProxyTest < ActiveRecord::TestCase end def test_scoped_allows_conditions - assert developers(:david).projects.merge!(where: 'foo').where_values.include?('foo') + assert developers(:david).projects.merge(where: 'foo').to_sql.include?('foo') end test "getting a scope from an association" do diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 53bd58e22e..2aeb2601c2 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -12,11 +12,11 @@ module ActiveRecord super(delegate) end - def type_cast_from_user(value) + def cast(value) "#{super} #{@decoration}" end - alias type_cast_from_database type_cast_from_user + alias deserialize cast end setup do @@ -28,7 +28,7 @@ module ActiveRecord teardown do return unless @connection - @connection.drop_table 'attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model' + @connection.drop_table 'attribute_decorators_model', if_exists: true Model.attribute_type_decorations.clear Model.reset_column_information end @@ -51,7 +51,7 @@ module ActiveRecord end test "undecorated columns are not touched" do - Model.attribute :another_string, Type::String.new, default: 'something or other' + Model.attribute :another_string, :string, default: 'something or other' Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } assert_equal 'something or other', Model.new.another_string @@ -86,7 +86,7 @@ module ActiveRecord end test "decorating attributes does not modify parent classes" do - Model.attribute :another_string, Type::String.new, default: 'whatever' + Model.attribute :another_string, :string, default: 'whatever' Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } child_class = Class.new(Model) child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) } @@ -102,15 +102,15 @@ module ActiveRecord end class Multiplier < SimpleDelegator - def type_cast_from_user(value) + def cast(value) return if value.nil? value * 2 end - alias type_cast_from_database type_cast_from_user + alias deserialize cast end test "decorating with a proc" do - Model.attribute :an_int, Type::Integer.new + Model.attribute :an_int, :integer type_is_integer = proc { |_, type| type.type == :integer } Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| Multiplier.new(type) diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index e38b32d7fc..74e556211b 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -17,7 +17,7 @@ module ActiveRecord include ActiveRecord::AttributeMethods - def self.column_names + def self.attribute_names %w{ one two three } end @@ -25,11 +25,11 @@ module ActiveRecord end def self.columns - column_names.map { FakeColumn.new(name) } + attribute_names.map { FakeColumn.new(name) } end def self.columns_hash - Hash[column_names.map { |name| + Hash[attribute_names.map { |name| [name, FakeColumn.new(name)] }] end @@ -39,13 +39,13 @@ module ActiveRecord def test_define_attribute_methods instance = @klass.new - @klass.column_names.each do |name| + @klass.attribute_names.each do |name| assert !instance.methods.map(&:to_s).include?(name) end @klass.define_attribute_methods - @klass.column_names.each do |name| + @klass.attribute_names.each do |name| assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined" end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 01ee1234a2..ea2b94cbf4 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -657,7 +657,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_attribute_in_current_time_zone + def test_setting_time_zone_aware_datetime_in_current_time_zone utc_time = Time.utc(2008, 1, 1) in_time_zone "Pacific Time (US & Canada)" do record = @target.new @@ -676,6 +676,47 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_setting_time_zone_aware_time_in_current_time_zone + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new + time_string = "10:00:00" + expected_time = Time.zone.parse("2000-01-01 #{time_string}") + + record.bonus_time = time_string + assert_equal expected_time, record.bonus_time + assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone + + record.bonus_time = '' + assert_nil record.bonus_time + end + end + + def test_setting_time_zone_aware_time_with_dst + in_time_zone "Pacific Time (US & Canada)" do + current_time = Time.zone.local(2014, 06, 15, 10) + record = @target.new(bonus_time: current_time) + time_before_save = record.bonus_time + + record.save + record.reload + + assert_equal time_before_save, record.bonus_time + assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone + end + end + + def test_removing_time_zone_aware_types + with_time_zone_aware_types(:datetime) do + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new(bonus_time: "10:00:00") + expected_time = Time.utc(2000, 01, 01, 10) + + assert_equal expected_time, record.bonus_time + assert record.bonus_time.utc? + end + end + end + def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable Topic.skip_time_zone_conversion_for_attributes = [:field_a] Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] @@ -888,6 +929,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_not_equal ['id'], @target.column_names end + def test_came_from_user + model = @target.first + + assert_not model.id_came_from_user? + model.id = "omg" + assert model.id_came_from_user? + end + + def test_accessed_fields + model = @target.first + + assert_equal [], model.accessed_fields + + model.title + + assert_equal ["title"], model.accessed_fields + end + private def new_topic_like_ar_class(&block) @@ -900,6 +959,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase klass end + def with_time_zone_aware_types(*types) + old_types = ActiveRecord::Base.time_zone_aware_types + ActiveRecord::Base.time_zone_aware_types = types + yield + ensure + ActiveRecord::Base.time_zone_aware_types = old_types + end + def cached_columns Topic.columns.map(&:name) end diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index ba53612d30..9d927481ec 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -65,6 +65,16 @@ module ActiveRecord assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) end + test "to_hash maintains order" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '2.2', bar: '3.3') + + attributes[:bar] + hash = attributes.to_h + + assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a + end + test "values_before_type_cast" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) attributes = builder.build_from_database(foo: '1.1', bar: '2.2') @@ -141,12 +151,12 @@ module ActiveRecord end class MyType - def type_cast_from_user(value) + def cast(value) return if value.nil? value + " from user" end - def type_cast_from_database(value) + def deserialize(value) return if value.nil? value + " from database" end @@ -186,5 +196,16 @@ module ActiveRecord attributes.freeze assert_equal({ foo: "1" }, attributes.to_hash) end + + test "#accessed_attributes returns only attributes which have been read" do + builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + + assert_equal [], attributes.accessed + + attributes.fetch_value(:foo) + + assert_equal [:foo], attributes.accessed + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 39a976fcc8..aa419c7a67 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end test "from_database + read type casts from database" do - @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + @type.expect(:deserialize, 'type cast from database', ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) type_cast_value = attribute.value @@ -22,7 +22,7 @@ module ActiveRecord end test "from_user + read type casts from user" do - @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + @type.expect(:cast, 'type cast from user', ['a value']) attribute = Attribute.from_user(nil, 'a value', @type) type_cast_value = attribute.value @@ -31,7 +31,7 @@ module ActiveRecord end test "reading memoizes the value" do - @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + @type.expect(:deserialize, 'from the database', ['whatever']) attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_value = attribute.value @@ -42,7 +42,7 @@ module ActiveRecord end test "reading memoizes falsy values" do - @type.expect(:type_cast_from_database, false, ['whatever']) + @type.expect(:deserialize, false, ['whatever']) attribute = Attribute.from_database(nil, 'whatever', @type) attribute.value @@ -58,27 +58,27 @@ module ActiveRecord end test "from_database + read_for_database type casts to and from database" do - @type.expect(:type_cast_from_database, 'read from database', ['whatever']) - @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + @type.expect(:deserialize, 'read from database', ['whatever']) + @type.expect(:serialize, 'ready for database', ['read from database']) attribute = Attribute.from_database(nil, 'whatever', @type) - type_cast_for_database = attribute.value_for_database + serialize = attribute.value_for_database - assert_equal 'ready for database', type_cast_for_database + assert_equal 'ready for database', serialize end test "from_user + read_for_database type casts from the user to the database" do - @type.expect(:type_cast_from_user, 'read from user', ['whatever']) - @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + @type.expect(:cast, 'read from user', ['whatever']) + @type.expect(:serialize, 'ready for database', ['read from user']) attribute = Attribute.from_user(nil, 'whatever', @type) - type_cast_for_database = attribute.value_for_database + serialize = attribute.value_for_database - assert_equal 'ready for database', type_cast_for_database + assert_equal 'ready for database', serialize end test "duping dups the value" do - @type.expect(:type_cast_from_database, 'type cast', ['a value']) + @type.expect(:deserialize, 'type cast', ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) value_from_orig = attribute.value @@ -90,7 +90,7 @@ module ActiveRecord end test "duping does not dup the value if it is not dupable" do - @type.expect(:type_cast_from_database, false, ['a value']) + @type.expect(:deserialize, false, ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) assert_same attribute.value, attribute.dup.value @@ -102,11 +102,11 @@ module ActiveRecord end class MyType - def type_cast_from_user(value) + def cast(value) value + " from user" end - def type_cast_from_database(value) + def deserialize(value) value + " from database" end end @@ -169,5 +169,24 @@ module ActiveRecord second = Attribute.from_user(:foo, 1, Type::Integer.new) assert_not_equal first, second end + + test "an attribute has not been read by default" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + assert_not attribute.has_been_read? + end + + test "an attribute has been read when its value is calculated" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + attribute.value + assert attribute.has_been_read? + end + + test "an attribute can not be mutated if it has not been read, + and skips expensive calculations" do + type_which_raises_from_all_methods = Object.new + attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) + + assert_not attribute.changed_in_place_from?("bar") + end end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index dbe1eb48db..eeda9335ad 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -1,17 +1,17 @@ require 'cases/helper' class OverloadedType < ActiveRecord::Base - attribute :overloaded_float, Type::Integer.new - attribute :overloaded_string_with_limit, Type::String.new(limit: 50) - attribute :non_existent_decimal, Type::Decimal.new - attribute :string_with_default, Type::String.new, default: 'the overloaded default' + attribute :overloaded_float, :integer + attribute :overloaded_string_with_limit, :string, limit: 50 + attribute :non_existent_decimal, :decimal + attribute :string_with_default, :string, default: 'the overloaded default' end class ChildOfOverloadedType < OverloadedType end class GrandchildOfOverloadedType < ChildOfOverloadedType - attribute :overloaded_float, Type::Float.new + attribute :overloaded_float, :float end class UnoverloadedType < ActiveRecord::Base @@ -50,8 +50,8 @@ module ActiveRecord end test "overloaded properties with limit" do - assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit - assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 50, OverloadedType.type_for_attribute('overloaded_string_with_limit').limit + assert_equal 255, UnoverloadedType.type_for_attribute('overloaded_string_with_limit').limit end test "nonexistent attribute" do @@ -87,29 +87,90 @@ module ActiveRecord assert_equal 4.4, data.overloaded_float end - test "overloading properties does not change column order" do - column_names = OverloadedType.column_names - assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names + test "overloading properties does not attribute method order" do + attribute_names = OverloadedType.attribute_names + assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), attribute_names end test "caches are cleared" do klass = Class.new(OverloadedType) - assert_equal 6, klass.columns.length - assert_not klass.columns_hash.key?('wibble') - assert_equal 6, klass.column_types.length + assert_equal 6, klass.attribute_types.length assert_equal 6, klass.column_defaults.length - assert_not klass.column_names.include?('wibble') - assert_equal 5, klass.content_columns.length + assert_not klass.attribute_types.include?('wibble') klass.attribute :wibble, Type::Value.new - assert_equal 7, klass.columns.length - assert klass.columns_hash.key?('wibble') - assert_equal 7, klass.column_types.length + assert_equal 7, klass.attribute_types.length assert_equal 7, klass.column_defaults.length - assert klass.column_names.include?('wibble') - assert_equal 6, klass.content_columns.length + assert klass.attribute_types.include?('wibble') + end + + test "the given default value is cast from user" do + custom_type = Class.new(Type::Value) do + def cast(*) + "from user" + end + + def deserialize(*) + "from database" + end + end + + klass = Class.new(OverloadedType) do + attribute :wibble, custom_type.new, default: "default" + end + model = klass.new + + assert_equal "from user", model.wibble + end + + test "procs for default values" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + assert_equal 1, klass.new.counter + assert_equal 2, klass.new.counter + end + + test "user provided defaults are persisted even if unchanged" do + model = OverloadedType.create! + + assert_equal "the overloaded default", model.reload.string_with_default + end + + if current_adapter?(:PostgreSQLAdapter) + test "arrays 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 + end + + string_array = ConnectionAdapters::PostgreSQL::OID::Array.new( + Type::String.new(limit: 50)) + int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( + Type::Integer.new) + refute_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 + + test "range types can be specified" do + klass = Class.new(OverloadedType) do + attribute :my_range, :string, limit: 50, range: true + attribute :my_int_range, :integer, range: true + end + + string_range = ConnectionAdapters::PostgreSQL::OID::Range.new( + Type::String.new(limit: 50)) + int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( + Type::Integer.new) + refute_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 end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 04d5a2869c..80b5a0004d 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -43,7 +43,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase reference = Class.new(ActiveRecord::Base) { self.table_name = "references" def self.name; 'Reference'; end - belongs_to :person, autosave: true, class: person + belongs_to :person, autosave: true, anonymous_class: person } u = person.create!(first_name: 'cool') @@ -629,7 +629,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -637,7 +637,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end teardown do - # We are running without transactional fixtures and need to cleanup. + # We are running without transactional tests and need to cleanup. Bird.delete_all Parrot.delete_all @ship.delete @@ -1009,7 +1009,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1030,6 +1030,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_equal 'The Vile Insanity', @pirate.reload.ship.name end + def test_changed_for_autosave_should_handle_cycles + @ship.pirate = @pirate + assert_queries(0) { @ship.save! } + + @parrot = @pirate.parrots.create(name: "some_name") + @parrot.name="changed_name" + assert_queries(1) { @ship.save! } + assert_queries(0) { @ship.save! } + end + def test_should_automatically_save_bang_the_associated_model @pirate.ship.name = 'The Vile Insanity' @pirate.save! @@ -1051,11 +1061,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_not_ignore_different_error_messages_on_the_same_attribute + old_validators = Ship._validators.deep_dup + old_callbacks = Ship._validate_callbacks.deep_dup Ship.validates_format_of :name, :with => /\w/ @pirate.ship.name = "" @pirate.catchphrase = nil assert @pirate.invalid? assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"] + ensure + Ship._validators = old_validators if old_validators + Ship._validate_callbacks = old_callbacks if old_callbacks end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -1130,7 +1145,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1151,7 +1166,7 @@ class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCas end class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1263,6 +1278,16 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) end + def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not + parrot = Parrot.create!(name: "Polly") + parrot.name = "Squawky" + pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr") + + pirate.save! + + assert_equal "Squawky", parrot.reload.name + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = '' } @@ -1399,7 +1424,7 @@ module AutosaveAssociationOnACollectionAssociationTests end class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1415,7 +1440,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1432,7 +1457,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T end class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1449,7 +1474,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedA end class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1466,7 +1491,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te end class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1489,7 +1514,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes end class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1510,7 +1535,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: end class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1533,7 +1558,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test end class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 0debd30e5c..31c31e4329 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# -*- coding: utf-8 -*- require "cases/helper" require 'active_support/concurrency/latch' @@ -813,7 +813,6 @@ class BasicsTest < ActiveRecord::TestCase def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts - author.send(:clear_association_cache) author_dup = author.dup assert_equal [], author_dup.posts @@ -904,8 +903,8 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - attribute :my_house_population, Type::Integer.new - attribute :atoms_in_universe, Type::Integer.new + attribute :my_house_population, :integer + attribute :atoms_in_universe, :integer end def test_big_decimal_conditions @@ -1010,54 +1009,61 @@ class BasicsTest < ActiveRecord::TestCase end def test_switching_between_table_name + k = Class.new(Joke) + assert_difference("GoodJoke.count") do - Joke.table_name = "cold_jokes" - Joke.create + k.table_name = "cold_jokes" + k.create - Joke.table_name = "funny_jokes" - Joke.create + k.table_name = "funny_jokes" + k.create end end def test_clear_cash_when_setting_table_name - Joke.table_name = "cold_jokes" - before_columns = Joke.columns - before_seq = Joke.sequence_name + original_table_name = Joke.table_name Joke.table_name = "funny_jokes" + before_columns = Joke.columns + before_seq = Joke.sequence_name + + Joke.table_name = "cold_jokes" after_columns = Joke.columns - after_seq = Joke.sequence_name + after_seq = Joke.sequence_name assert_not_equal before_columns, after_columns assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + ensure + Joke.table_name = original_table_name end def test_dont_clear_sequence_name_when_setting_explicitly - Joke.sequence_name = "black_jokes_seq" - Joke.table_name = "cold_jokes" - before_seq = Joke.sequence_name + k = Class.new(Joke) + k.sequence_name = "black_jokes_seq" + k.table_name = "cold_jokes" + before_seq = k.sequence_name - Joke.table_name = "funny_jokes" - after_seq = Joke.sequence_name + k.table_name = "funny_jokes" + after_seq = k.sequence_name assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? - ensure - Joke.reset_sequence_name end def test_dont_clear_inheritance_column_when_setting_explicitly - Joke.inheritance_column = "my_type" - before_inherit = Joke.inheritance_column + k = Class.new(Joke) + k.inheritance_column = "my_type" + before_inherit = k.inheritance_column - Joke.reset_column_information - after_inherit = Joke.inheritance_column + k.reset_column_information + after_inherit = k.inheritance_column assert_equal before_inherit, after_inherit unless before_inherit.blank? && after_inherit.blank? end def test_set_table_name_symbol_converted_to_string - Joke.table_name = :cold_jokes - assert_equal 'cold_jokes', Joke.table_name + k = Class.new(Joke) + k.table_name = :cold_jokes + assert_equal 'cold_jokes', k.table_name end def test_quoted_table_name_after_set_table_name @@ -1405,15 +1411,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_uniq_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:uniq => scope)) - assert_equal scope, Bird.uniq + assert_deprecated do + assert_equal Bird.all.distinct, Bird.uniq + end end def test_distinct_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:distinct => scope)) - assert_equal scope, Bird.distinct + assert_equal Bird.all.distinct, Bird.distinct end def test_table_name_with_2_abstract_subclasses @@ -1428,7 +1432,7 @@ class BasicsTest < ActiveRecord::TestCase attrs.delete 'id' typecast = Class.new(ActiveRecord::Type::Value) { - def type_cast value + def cast value "t.lo" end } diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c12fa03015..0791dde1f2 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -37,9 +37,9 @@ class EachTest < ActiveRecord::TestCase if Enumerator.method_defined? :size def test_each_should_return_a_sized_enumerator - assert_equal 11, Post.find_each(:batch_size => 1).size - assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size - assert_equal 11, Post.find_each(:batch_size => 10_000).size + assert_equal 11, Post.find_each(batch_size: 1).size + assert_equal 5, Post.find_each(batch_size: 2, begin_at: 7).size + assert_equal 11, Post.find_each(batch_size: 10_000).size end end @@ -99,7 +99,16 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_start_from_the_start_option assert_queries(@total) do - Post.find_in_batches(:batch_size => 1, :start => 2) do |batch| + Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end + + def test_find_in_batches_should_end_at_the_end_option + assert_queries(6) do + Post.find_in_batches(batch_size: 1, end_at: 5) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end @@ -163,7 +172,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){} end end @@ -172,7 +181,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.find_in_batches(:batch_size => 1, :start => start_nick) do |batch| + Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch| subscribers.concat(batch) end @@ -181,8 +190,9 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end @@ -200,11 +210,32 @@ class EachTest < ActiveRecord::TestCase end end + def test_find_in_batches_start_deprecated + assert_deprecated do + assert_queries(@total) do + Post.find_in_batches(batch_size: 1, start: 2) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end + end + + def test_find_each_start_deprecated + assert_deprecated do + assert_queries(@total) do + Post.find_each(batch_size: 1, start: 2) do |post| + assert_kind_of Post, post + end + end + end + end + if Enumerator.method_defined? :size def test_find_in_batches_should_return_a_sized_enumerator assert_equal 11, Post.find_in_batches(:batch_size => 1).size assert_equal 6, Post.find_in_batches(:batch_size => 2).size - assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size + assert_equal 4, Post.find_in_batches(batch_size: 2, begin_at: 4).size assert_equal 4, Post.find_in_batches(:batch_size => 3).size assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index ccf2be369d..86dee929bf 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" # Without using prepared statements, it makes no sense to test diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 66663b3e0e..1e38b97c4a 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -22,8 +22,8 @@ module ActiveRecord def setup super @connection = ActiveRecord::Base.connection - @subscriber = LogListener.new - @pk = Topic.columns_hash[Topic.primary_key] + @subscriber = LogListener.new + @pk = Topic.columns_hash[Topic.primary_key] @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end @@ -40,7 +40,7 @@ module ActiveRecord def test_binds_are_logged sub = @connection.substitute_at(@pk) - binds = [[@pk, 1]] + binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] sql = "select * from topics where id = #{sub.to_sql}" @connection.exec_query(sql, 'SQL', binds) @@ -49,29 +49,17 @@ module ActiveRecord assert_equal binds, message[4][:binds] end - def test_binds_are_logged_after_type_cast - sub = @connection.substitute_at(@pk) - binds = [[@pk, "3"]] - sql = "select * from topics where id = #{sub.to_sql}" - - @connection.exec_query(sql, 'SQL', binds) - - message = @subscriber.calls.find { |args| args[4][:sql] == sql } - assert_equal [[@pk, 3]], message[4][:binds] - end - def test_find_one_uses_binds Topic.find(1) - binds = [[@pk, 1]] - message = @subscriber.calls.find { |args| args[4][:binds] == binds } + message = @subscriber.calls.find { |args| args[4][:binds].any? { |attr| attr.value == 1 } } assert message, 'expected a message with binds' end - def test_logs_bind_vars + def test_logs_bind_vars_after_type_cast payload = { :name => 'SQL', :sql => 'select * from topics where id = ?', - :binds => [[@pk, 10]] + :binds => [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] } event = ActiveSupport::Notifications::Event.new( 'foo', diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 299217214e..cb4681109c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -11,13 +11,17 @@ require 'models/minivan' require 'models/speedometer' require 'models/ship_part' require 'models/treasure' +require 'models/developer' +require 'models/comment' +require 'models/rating' +require 'models/post' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - attribute :world_population, Type::Integer.new - attribute :my_house_population, Type::Integer.new - attribute :atoms_in_universe, Type::Integer.new + attribute :world_population, :integer + attribute :my_house_population, :integer + attribute :atoms_in_universe, :integer end class CalculationsTest < ActiveRecord::TestCase @@ -355,7 +359,10 @@ class CalculationsTest < ActiveRecord::TestCase def test_count_with_distinct assert_equal 4, Account.select(:credit_limit).distinct.count - assert_equal 4, Account.select(:credit_limit).uniq.count + + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).uniq.count + end end def test_count_with_aliased_attribute @@ -500,8 +507,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [ topic.written_on ], relation.pluck(:written_on) end - def test_pluck_and_uniq - assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit) + def test_pluck_and_distinct + assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit) end def test_pluck_in_relation @@ -625,10 +632,43 @@ 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! assert_equal({ "has trinket" => part.id }, ShipPart.joins(:trinkets).group("ship_parts.name").sum(:id)) end + + def test_calculation_grouped_by_association_doesnt_error_when_no_records_have_association + Client.update_all(client_of: nil) + assert_equal({ nil => Client.count }, Client.group(:firm).count) + end + + def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association + assert_nothing_raised ActiveRecord::StatementInvalid do + developer = Developer.create!(name: 'developer') + developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count + end + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 670d94dc06..73ac30e547 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -288,7 +288,12 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], - [ :after_save, :block ] + [ :after_save, :block ], + [ :after_commit, :block ], + [ :after_commit, :object ], + [ :after_commit, :proc ], + [ :after_commit, :string ], + [ :after_commit, :method ] ], david.history end @@ -357,7 +362,12 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], - [ :after_save, :block ] + [ :after_save, :block ], + [ :after_commit, :block ], + [ :after_commit, :object ], + [ :after_commit, :proc ], + [ :after_commit, :string ], + [ :after_commit, :method ] ], david.history end @@ -408,7 +418,12 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_destroy, :string ], [ :after_destroy, :proc ], [ :after_destroy, :object ], - [ :after_destroy, :block ] + [ :after_destroy, :block ], + [ :after_commit, :block ], + [ :after_commit, :object ], + [ :after_commit, :proc ], + [ :after_commit, :string ], + [ :after_commit, :method ] ], david.history end @@ -436,6 +451,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal exc.record, david + assert_equal "Failed to save the record", exc.message end david = ImmutableDeveloper.find(1) @@ -479,6 +495,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal exc.record, david + assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index bcfd66b4bf..14b95ecab1 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -14,7 +14,7 @@ module ActiveRecord # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, Type::String.new(limit: 20)) + column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -22,7 +22,7 @@ module ActiveRecord end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", Type::String.new(limit: 20)) + column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -30,94 +30,53 @@ module ActiveRecord end def test_should_specify_not_null_if_null_option_is_false - column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false) + type_metadata = SqlTypeMetadata.new(limit: 20) + column = Column.new("title", "Hello", type_metadata, false) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)") + type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") + binary_column = AbstractMysqlAdapter::Column.new("title", "a", type) assert_equal "a", binary_column.default - varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") + type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") + varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type) assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob") + AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) end + text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new( + SqlTypeMetadata.new(type: :text)) assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "Hello", Type::Text.new) + AbstractMysqlAdapter::Column.new("title", "Hello", text_type) end - text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) + text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) assert_equal nil, text_column.default - not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false) + not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob") + binary_type = SqlTypeMetadata.new(sql_type: "blob") + blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type) assert !blob_column.has_default? - text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) + text_type = SqlTypeMetadata.new(type: :text) + text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) assert !text_column.has_default? end end - - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)") - assert_equal "a", binary_column.default - - varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") - assert_equal "a", varbinary_column.default - end - - def test_should_not_set_default_for_blob_and_text_data_types - assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob") - end - - assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "Hello", Type::Text.new) - end - - text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) - assert_equal nil, text_column.default - - not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false) - assert_equal "", not_null_text_column.default - end - - def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob") - assert !blob_column.has_default? - - text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) - assert !text_column.has_default? - end - end - - if current_adapter?(:PostgreSQLAdapter) - def test_bigint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Integer.new - bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint") - assert_equal :integer, bigint_column.type - end - - def test_smallint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Integer.new - smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") - assert_equal :integer, smallint_column.type - end - end end end end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 662e19f35e..580568c8ac 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -6,7 +6,7 @@ module ActiveRecord class Pool < ConnectionPool def insert_connection_for_test!(c) synchronize do - @connections << c + adopt_connection(c) @available.add c end end @@ -24,7 +24,9 @@ module ActiveRecord def test_lease_twice assert @adapter.lease, 'should lease adapter' - assert_not @adapter.lease, 'should not lease adapter' + assert_raises(ActiveRecordError) do + @adapter.lease + end end def test_expire_mutates_in_use diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b72f8ca88c..9b1865e8bb 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -46,6 +46,52 @@ module ActiveRecord def test_connection_pools assert_equal([@pool], @handler.connection_pools) end + + if Process.respond_to?(:fork) + def test_connection_pool_per_pid + object_id = ActiveRecord::Base.connection.object_id + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end + + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool + @pool.schema_cache = @pool.connection.schema_cache + @pool.schema_cache.add('posts') + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + pool = @handler.retrieve_connection_pool(@klass) + wr.write Marshal.dump pool.schema_cache.size + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_equal @pool.schema_cache.size, Marshal.load(rd.read) + rd.close + end + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index d5c1dc1e5d..7566863653 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -79,13 +79,22 @@ module ActiveRecord assert_lookup_type :integer, 'bigint' end + def test_bigint_limit + cast_type = @connection.type_map.lookup("bigint") + if current_adapter?(:OracleAdapter) + assert_equal 19, cast_type.limit + else + assert_equal 8, cast_type.limit + end + end + def test_decimal_without_scale types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} types.each do |type| cast_type = @connection.type_map.lookup(type) assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.type_cast_from_user(2.1) + assert_equal 2, cast_type.cast(2.1) end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f53c496ecd..dff6ea0fb0 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -26,29 +26,6 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - if Process.respond_to?(:fork) - def test_connection_pool_per_pid - object_id = ActiveRecord::Base.connection.object_id - - rd, wr = IO.pipe - rd.binmode - wr.binmode - - pid = fork { - rd.close - wr.write Marshal.dump ActiveRecord::Base.connection.object_id - wr.close - exit! - } - - wr.close - - Process.waitpid pid - assert_not_equal object_id, Marshal.load(rd.read) - rd.close - end - end - def test_app_delegation manager = ConnectionManagement.new(@app) @@ -110,7 +87,7 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - test "proxy is polite to it's body and responds to it" do + test "proxy is polite to its body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..c905772193 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -100,7 +100,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first connection.close @@ -112,7 +112,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first @pool.remove connection @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request # even while well under capacity in a multi-threaded environment # such as a Java servlet container. # # We don't need strict fairness: if two connections become - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in @@ -234,7 +234,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -271,7 +271,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -341,6 +341,185 @@ module ActiveRecord handler.establish_connection anonymous, nil } end + + def test_pool_sets_connection_schema_cache + connection = pool.checkout + schema_cache = SchemaCache.new connection + schema_cache.add(:posts) + pool.schema_cache = schema_cache + + pool.with_connection do |conn| + assert_not_same pool.schema_cache, conn.schema_cache + assert_equal pool.schema_cache.size, conn.schema_cache.size + assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) + end + + pool.checkin connection + end + + 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 + + @pool.singleton_class.class_eval do + define_method(:new_connection) do + all_threads_in_new_connection.release + all_go.await + super() + end + end + + connecting_threads = [] + @pool.size.times do + connecting_threads << Thread.new { @pool.checkout } + end + + begin + Timeout.timeout(5) do + # 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 + 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 + connecting_threads.map(&:join) + end + end + + def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect, :clear_reloadable_connections].each do |group_action_method| + @pool.with_connection do |connection| + assert_raises(ExclusiveConnectionTimeoutError) do + Thread.new { @pool.send(group_action_method) }.join + end + end + end + end + + def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns + [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| + begin + thread = timed_join_result = nil + @pool.with_connection do |connection| + thread = Thread.new { @pool.send(group_action_method) } + + # give the other `thread` some time to get stuck in `group_action_method` + timed_join_result = thread.join(0.3) + # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to + # release our connection + assert_nil timed_join_result + + # assert that since this is within default timeout our connection hasn't been forcefully taken away from us + assert @pool.active_connection? + end + ensure + thread.join if thread && !timed_join_result # clean up the other thread + end + end + end + + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect!, :clear_reloadable_connections!].each do |group_action_method| + @pool.with_connection do |connection| + Thread.new { @pool.send(group_action_method) }.join + # assert connection has been forcefully taken away from us + assert_not @pool.active_connection? + end + end + end + + def test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads + 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 + + # 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 } + end + + # wait for first_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + # create a different, later thread, that will attempt to do a "group action", + # but because of the group action semantics it should be able to preempt the + # first_thread when a connection is made available + second_thread = Thread.new do + pool.send(group_action_method) + second_thread_done.release + end + + # wait for second_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 2 + + # return the only available connection + pool.checkin(conn) + + # if the second_thread is not able to preempt the first_thread, + # they will temporarily (until either of them timeouts with ConnectionTimeoutError) + # deadlock and a join(2) timeout will be reached + failed = true unless second_thread.join(2) + + #--- post test clean up start + second_thread_done.release if failed + + # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for + # it to timeout with ConnectionTimeoutError + if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 + pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + end + + first_thread.join + second_thread.join + #--- post test clean up end + + flunk "#{group_action_method} is not able to preempt other waiting threads" if failed + end + end + end + + def test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary + with_single_connection_pool do |pool| + conn = pool.connection # drain the only available connection + def conn.requires_reloading? # make sure it gets removed from the pool by clear_reloadable_connections + true + end + + stuck_thread = Thread.new do + pool.with_connection {} + end + + # wait for stuck_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + pool.clear_reloadable_connections + + unless stuck_thread.join(2) + flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue' + end + + assert_equal 0, pool.num_waiting_in_queue + end + end + + private + def with_single_connection_pool + one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup + one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config + yield(pool = ConnectionPool.new(one_conn_spec)) + ensure + pool.disconnect! if pool + end end end end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 715d92af99..3cb98832c5 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -98,4 +98,15 @@ class CoreTest < ActiveRecord::TestCase assert actual.start_with?(expected.split('XXXXXX').first) assert actual.end_with?(expected.split('XXXXXX').last) end + + def test_pretty_print_overridden_by_inspect + subtopic = Class.new(Topic) do + def inspect + "inspecting topic" + end + end + actual = '' + PP.pp(subtopic.new, StringIO.new(actual)) + assert_equal "inspecting topic\n", actual + end end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 07a182070b..1f5055b2a2 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -180,4 +180,22 @@ class CounterCacheTest < ActiveRecord::TestCase SpecialTopic.reset_counters(special.id, :lightweight_special_replies) end end + + test "counters are updated both in memory and in the database on create" do + car = Car.new(engines_count: 0) + car.engines = [Engine.new, Engine.new] + car.save! + + assert_equal 2, car.engines_count + assert_equal 2, car.reload.engines_count + end + + test "counter caches are updated in memory when the default value is nil" do + car = Car.new(engines_count: nil) + car.engines = [Engine.new, Engine.new] + car.save! + + assert_equal 2, car.engines_count + assert_equal 2, car.reload.engines_count + end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb new file mode 100644 index 0000000000..698f1b852e --- /dev/null +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -0,0 +1,111 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_datetime_with_precision? +class DateTimePrecisionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + class Foo < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + end + + teardown do + @connection.drop_table :foos, if_exists: true + end + + def test_datetime_data_type_with_precision + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :created_at, :datetime, precision: 0 + @connection.add_column :foos, :updated_at, :datetime, precision: 5 + assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_timestamps_helper_with_custom_precision + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 4 + end + assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_passing_precision_to_datetime_does_not_set_limit + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 4 + end + assert_nil activerecord_column_option('foos', 'created_at', 'limit') + assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + end + + def test_invalid_datetime_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 7 + end + end + end + + def test_database_agrees_with_activerecord_about_precision + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 4 + end + assert_equal 4, database_datetime_precision('foos', 'created_at') + assert_equal 4, database_datetime_precision('foos', 'updated_at') + end + + def test_formatting_datetime_according_to_precision + @connection.create_table(:foos, force: true) do |t| + t.datetime :created_at, precision: 0 + t.datetime :updated_at, precision: 4 + end + date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) + Foo.create!(created_at: date, updated_at: date) + assert foo = Foo.find_by(created_at: date) + assert_equal 1, Foo.where(updated_at: date).count + assert_equal date.to_s, foo.created_at.to_s + assert_equal date.to_s, foo.updated_at.to_s + assert_equal 000000, foo.created_at.usec + assert_equal 999900, foo.updated_at.usec + end + + def test_schema_dump_includes_datetime_precision + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 6 + end + output = dump_table_schema("foos") + assert_match %r{t\.datetime\s+"created_at",\s+precision: 6,\s+null: false$}, output + assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output + end + + if current_adapter?(:PostgreSQLAdapter) + def test_datetime_precision_with_zero_should_be_dumped + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 0 + end + output = dump_table_schema("foos") + assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output + assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output + end + end + + private + + def database_datetime_precision(table_name, column_name) + results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"].to_i + end + + def activerecord_column_option(tablename, column_name, option) + result = @connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end +end +end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 330232cee2..4cbff564aa 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -55,7 +55,7 @@ class DateTimeTest < ActiveRecord::TestCase now = DateTime.now with_timezone_config default: :local do task = Task.new starting: now - assert now, task.starting + assert_equal now, task.starting end end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index e9bc583bf4..67fddebf45 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -40,7 +40,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase end teardown do - @connection.drop_table "default_numbers" if @connection.table_exists? 'default_numbers' + @connection.drop_table :default_numbers, if_exists: true end def test_default_positive_integer @@ -90,14 +90,14 @@ end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will - # open a new transaction. When in transactional fixtures mode, this will + # open a new transaction. When in transactional tests mode, this will # cause Active Record to create a new savepoint. However, since MySQL doesn't # support DDL transactions, creating a table will result in any created # savepoints to be automatically released. This in turn causes the savepoint # release code in AbstractAdapter#transaction to fail. # - # We don't want that to happen, so we disable transactional fixtures here. - self.use_transactional_fixtures = false + # We don't want that to happen, so we disable transactional tests here. + self.use_transactional_tests = false def using_strict(strict) connection = ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 5b71bd7e67..f5aaf22e13 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -623,32 +623,6 @@ class DirtyTest < ActiveRecord::TestCase end end - test "defaults with type that implements `type_cast_for_database`" do - type = Class.new(ActiveRecord::Type::Value) do - def type_cast(value) - value.to_i - end - - def type_cast_for_database(value) - value.to_s - end - end - - model_class = Class.new(ActiveRecord::Base) do - self.table_name = 'numeric_data' - attribute :foo, type.new, default: 1 - end - - model = model_class.new - assert_not model.foo_changed? - - model = model_class.new(foo: 1) - assert_not model.foo_changed? - - model = model_class.new(foo: '1') - assert_not model.foo_changed? - end - test "in place mutation detection" do pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" @@ -708,6 +682,43 @@ class DirtyTest < ActiveRecord::TestCase assert model.first_name_changed? end + test "attribute_will_change! doesn't try to save non-persistable attributes" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + attribute :non_persisted_attribute, :string + end + + record = klass.new(first_name: "Sean") + record.non_persisted_attribute_will_change! + + assert record.non_persisted_attribute_changed? + assert record.save + end + + test "mutating and then assigning doesn't remove the change" do + pirate = Pirate.create!(catchphrase: "arrrr") + pirate.catchphrase << " matey!" + pirate.catchphrase = "arrrr matey!" + + 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/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 94447addc1..c25089a420 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestDisconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @connection = ActiveRecord::Base.connection @@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection.execute "SELECT count(*) from products" @connection.disconnect! assert_raises(ActiveRecord::StatementInvalid) do - @connection.execute "SELECT count(*) from products" + silence_warnings do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 346fcab6ea..769b171717 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -9,26 +9,83 @@ class EnumTest < ActiveRecord::TestCase end test "query state by predicate" do - assert @book.proposed? + assert @book.published? assert_not @book.written? - assert_not @book.published? + assert_not @book.proposed? - assert @book.unread? + 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 "proposed", @book.status - assert_equal "unread", @book.read_status + 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.proposed.first - assert_equal @book, Book.unread.first + 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_equal @book, Book.where(status: [published]).first + refute_equal @book, Book.where(status: [written]).first + refute_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_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_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_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_equal @book, Book.where.not(status: "written").first + end + + test "build from scope" do + assert Book.written.build.written? + refute 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 Book.where(status: :written).build.written? + refute Book.where(status: :written).build.proposed? + assert Book.where(status: "written").build.written? + refute 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 @@ -53,42 +110,61 @@ class EnumTest < ActiveRecord::TestCase test "enum changed attributes" do old_status = @book.status - @book.status = :published + 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 - @book.status = :published - assert_equal [old_status, 'published'], @book.changes[: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 = :published + @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 = :published - assert @book.attribute_changed?(:status, to: 'published') + @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 - @book.status = :published + 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 - @book.status = :published - assert @book.attribute_changed?(:status, from: old_status, to: 'published') + 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 @@ -98,7 +174,7 @@ class EnumTest < ActiveRecord::TestCase end test "persist changes that are dirty" do - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = :written assert @book.attribute_changed?(:status) @@ -106,7 +182,7 @@ class EnumTest < ActiveRecord::TestCase test "reverted changes that are not dirty" do old_status = @book.status - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = old_status assert_not @book.attribute_changed?(:status) @@ -129,19 +205,24 @@ class EnumTest < ActiveRecord::TestCase assert_equal "'unknown' is not a valid status", e.message end + test "NULL values from database should be casted to nil" do + Book.where(id: @book.id).update_all("status = NULL") + assert_nil @book.reload.status + end + test "assign nil value" do @book.status = nil - assert @book.status.nil? + assert_nil @book.status end test "assign empty string value" do @book.status = '' - assert @book.status.nil? + assert_nil @book.status end test "assign long empty string value" do @book.status = ' ' - assert @book.status.nil? + assert_nil @book.status end test "constant to access the mapping" do @@ -153,15 +234,23 @@ 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 - assert_equal "proposed", @book.status_before_type_cast + if @book.status_came_from_user? + assert_equal "published", @book.status_before_type_cast + else + assert_equal "published", @book.status + end end test "reserved enum names" do @@ -177,9 +266,10 @@ class EnumTest < ActiveRecord::TestCase ] conflicts.each_with_index do |name, i| - assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do + e = assert_raises(ArgumentError) do klass.class_eval { enum name => ["value_#{i}"] } end + assert_match(/You tried to define an enum named \"#{name}\" on the model/, e.message) end end @@ -199,9 +289,10 @@ class EnumTest < ActiveRecord::TestCase ] conflicts.each_with_index do |value, i| - assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do klass.class_eval { enum "status_#{i}" => [value] } end + assert_match(/You tried to define an enum named .* on the model/, e.message) end end @@ -287,4 +378,31 @@ class EnumTest < ActiveRecord::TestCase book2.status = :uploaded assert_equal ['drafted', 'uploaded'], book2.status_change end + + test "declare multiple enums at a time" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written, :published], + nullable_status: [:single, :married] + end + + book1 = klass.proposed.create! + assert book1.proposed? + + 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 end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 9d25bdd82a..f1d5511bb8 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -28,7 +28,7 @@ if ActiveRecord::Base.connection.supports_explain? assert_match "SELECT", sql if binds.any? assert_equal 1, binds.length - assert_equal "honda", binds.flatten.last + assert_equal "honda", binds.last.value else assert_match 'honda', sql end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 39308866ee..4b819a82e8 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -947,7 +947,6 @@ class FinderTest < ActiveRecord::TestCase end end - # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') assert_equal [], Post.where(id: nil) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 9edeb8b47f..03a187ae92 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' @@ -28,7 +29,7 @@ require 'tempfile' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false # other_topics fixture should not be included here fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights @@ -273,16 +274,16 @@ class HasManyThroughFixture < ActiveSupport::TestCase Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } end - def test_has_many_through + def test_has_many_through_with_default_table_name pt = make_model "ParrotTreasure" parrot = make_model "Parrot" treasure = make_model "Treasure" pt.table_name = "parrots_treasures" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -292,6 +293,24 @@ class HasManyThroughFixture < ActiveSupport::TestCase assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures'] end + def test_has_many_through_with_renamed_table + pt = make_model "ParrotTreasure" + parrot = make_model "Parrot" + treasure = make_model "Treasure" + + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure + + parrot.has_many :parrot_treasures, :anonymous_class => pt + parrot.has_many :treasures, :through => :parrot_treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + rows = fs.table_rows + assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrot_treasures'] + end + def load_has_and_belongs_to_many parrot = make_model "Parrot" parrot.has_and_belongs_to_many :treasures @@ -309,7 +328,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) fixtures :companies def setup - @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')] + @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting'), Course.new(name: 'Test')] ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized end @@ -401,7 +420,7 @@ end class TransactionalFixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true - self.use_transactional_fixtures = true + self.use_transactional_tests = true fixtures :topics @@ -493,7 +512,7 @@ class CheckSetTableNameFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_table_method assert_kind_of Joke, funny_jokes(:a_joke) @@ -505,7 +524,7 @@ class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase fixtures :items # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_named_accessor assert_kind_of Book, items(:dvd) @@ -517,7 +536,7 @@ class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase fixtures :items, :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_named_accessor_of_differently_named_fixture assert_kind_of Book, items(:dvd) @@ -531,7 +550,7 @@ end class CustomConnectionFixturesTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_leaky_destroy assert_nothing_raised { courses(:ruby) } @@ -546,7 +565,7 @@ end class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - self.use_transactional_fixtures = true + self.use_transactional_tests = true def test_leaky_destroy assert_nothing_raised { courses(:ruby) } @@ -562,7 +581,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our lack of set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_raises_error assert_raise ActiveRecord::FixtureClassNotFound do @@ -576,7 +595,7 @@ class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_proper_escaped_fixture assert_equal "The \\n Aristocrats\nAte the candy\n", funny_jokes(:another_joke).name @@ -646,7 +665,7 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase end class FasterFixturesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :categories, :authors def load_extra_fixture(name) @@ -672,7 +691,8 @@ class FasterFixturesTest < ActiveRecord::TestCase end class FoxyFixturesTest < ActiveRecord::TestCase - fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users" + fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, + :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' require 'models/uuid_parent' @@ -792,6 +812,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert_equal("X marks the spot!", pirates(:mark).catchphrase) end + def test_supports_label_interpolation_for_fixnum_label + assert_equal("#1 pirate!", pirates(1).catchphrase) + end + def test_supports_polymorphic_belongs_to assert_equal(pirates(:redbeard), treasures(:sapphire).looter) assert_equal(parrots(:louis), treasures(:ruby).looter) @@ -808,10 +832,23 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert_equal pirates(:blackbeard), parrots(:polly).killer end + def test_supports_sti_with_respective_files + assert_kind_of LiveParrot, live_parrots(:dusty) + assert_kind_of DeadParrot, dead_parrots(:deadbird) + assert_equal pirates(:blackbeard), dead_parrots(:deadbird).killer + end + def test_namespaced_models assert admin_accounts(:signals37).users.include?(admin_users(:david)) assert_equal 2, admin_accounts(:signals37).users.size end + + def test_resolves_enums + assert books(:awdr).published? + assert books(:awdr).read? + assert books(:rfr).proposed? + assert books(:ddd).published? + end end class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase @@ -830,9 +867,9 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase set_fixture_class :randomly_named_a9 => ClassNameThatDoesNotFollowCONVENTIONS, :'admin/randomly_named_a9' => - Admin::ClassNameThatDoesNotFollowCONVENTIONS, + Admin::ClassNameThatDoesNotFollowCONVENTIONS1, 'admin/randomly_named_b0' => - Admin::ClassNameThatDoesNotFollowCONVENTIONS + Admin::ClassNameThatDoesNotFollowCONVENTIONS2 fixtures :randomly_named_a9, 'admin/randomly_named_a9', :'admin/randomly_named_b0' @@ -843,15 +880,15 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase end def test_named_accessor_for_randomly_named_namespaced_fixture_and_class - assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS, + assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS1, admin_randomly_named_a9(:first_instance) - assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS, + assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS2, admin_randomly_named_b0(:second_instance) end def test_table_name_is_defined_in_the_model - assert_equal 'randomly_named_table', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name - assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name + assert_equal 'randomly_named_table2', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name + assert_equal 'randomly_named_table2', Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name end end @@ -867,3 +904,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 925491acbd..12c793c408 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -30,6 +30,9 @@ ARTest.connect # Quote "type" if it's a reserved word for the current connection. QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') +# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends. +ActiveRecord::Base.time_zone_aware_types << :time + def current_adapter?(*types) types.any? do |type| ActiveRecord::ConnectionAdapters.const_defined?(type) && @@ -43,7 +46,7 @@ def in_memory_db? end def mysql_56? - current_adapter?(:Mysql2Adapter) && + current_adapter?(:MysqlAdapter, :Mysql2Adapter) && ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" end @@ -121,7 +124,7 @@ def enable_extension!(extension, connection) return connection.reconnect! if connection.extension_enabled?(extension) connection.enable_extension extension - connection.commit_db_transaction + connection.commit_db_transaction if connection.transaction_open? connection.reconnect! end @@ -140,7 +143,7 @@ class ActiveSupport::TestCase self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false - self.use_transactional_fixtures = true + self.use_transactional_tests = true def create_fixtures(*fixture_set_names, &block) ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) @@ -200,8 +203,3 @@ module InTimeZone end require 'mocha/setup' # FIXME: stop using mocha - -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index b4617cf6f9..5ba9a1029a 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' class HotCompatibilityTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @klass = Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index fe6323ab02..f67d85603a 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -7,15 +7,32 @@ require 'models/subscriber' require 'models/vegetables' require 'models/shop' +module InheritanceTestHelper + def with_store_full_sti_class(&block) + assign_store_full_sti_class true, &block + end + + def without_store_full_sti_class(&block) + assign_store_full_sti_class false, &block + end + + def assign_store_full_sti_class(flag) + old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = flag + yield + ensure + ActiveRecord::Base.store_full_sti_class = old_store_full_sti_class + end +end + class InheritanceTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - assert_equal 'Namespaced::Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + assert_equal 'Namespaced::Company', Namespaced::Company.sti_name + end end def test_class_with_blank_sti_name @@ -33,39 +50,31 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_without_store_full_sti_class_returns_demodulized_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - assert_equal 'Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + assert_equal 'Company', Namespaced::Company.sti_name + end end def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - item = Namespaced::Company.new - assert_equal 'Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Company', item[:type] + end end def test_should_store_full_class_name_with_store_full_sti_class_option_enabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.new - assert_equal 'Namespaced::Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Namespaced::Company', item[:type] + end end def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.create :name => "Wolverine 2" - assert_not_nil Company.find(item.id) - assert_not_nil Namespaced::Company.find(item.id) - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.create name: "Wolverine 2" + assert_not_nil Company.find(item.id) + assert_not_nil Namespaced::Company.find(item.id) + end end def test_company_descends_from_active_record @@ -121,6 +130,12 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of Cabbage, cabbage end + def test_becomes_and_change_tracking_for_inheritance_columns + cucumber = Vegetable.find(1) + cabbage = cucumber.becomes!(Cabbage) + assert_equal ['Cucumber', 'Cabbage'], cabbage.custom_type_change + end + def test_alt_becomes_bang_resets_inheritance_type_column vegetable = Vegetable.create!(name: "Red Pepper") assert_nil vegetable.custom_type @@ -198,10 +213,28 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_unrelated_namespaced_type + without_store_full_sti_class do + e = assert_raises ActiveRecord::SubclassNotFound do + Namespaced::Company.new(type: 'Firm') + end + + assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message + end + end + + def test_new_with_complex_inheritance assert_nothing_raised { Client.new(type: 'VerySpecialClient') } end + def test_new_without_storing_full_sti_class + without_store_full_sti_class do + item = Company.new(type: 'SpecialCo') + assert_instance_of Company::SpecialCo, item + end + end + def test_new_with_autoload_paths path = File.expand_path('../../models/autoloadable', __FILE__) ActiveSupport::Dependencies.autoload_paths << path @@ -294,12 +327,12 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_something_inherited account = Account.all.merge!(:includes => :firm).find(1) - assert account.association_cache.key?(:firm), "nil proves eager load failed" + assert account.association(:firm).loaded?, "association was not eager loaded" end def test_alt_eager_loading cabbage = RedCabbage.all.merge!(:includes => :seller).find(4) - assert cabbage.association_cache.key?(:seller), "nil proves eager load failed" + assert cabbage.association(:seller).loaded?, "association was not eager loaded" end def test_eager_load_belongs_to_primary_key_quoting @@ -325,6 +358,7 @@ class InheritanceTest < ActiveRecord::TestCase end class InheritanceComputeTypeTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies def setup @@ -338,27 +372,26 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase end def test_instantiation_doesnt_try_to_require_corresponding_file - ActiveRecord::Base.store_full_sti_class = false - foo = Firm.first.clone - foo.type = 'FirmOnTheFly' - foo.save! + without_store_full_sti_class do + foo = Firm.first.clone + foo.type = 'FirmOnTheFly' + foo.save! - # Should fail without FirmOnTheFly in the type condition. - assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } + # Should fail without FirmOnTheFly in the type condition. + assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in the test case where Dependencies won't see it. - self.class.const_set :FirmOnTheFly, Class.new(Firm) - assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } + # Nest FirmOnTheFly in the test case where Dependencies won't see it. + self.class.const_set :FirmOnTheFly, Class.new(Firm) + assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in Firm where Dependencies will see it. - # This is analogous to nesting models in a migration. - Firm.const_set :FirmOnTheFly, Class.new(Firm) + # Nest FirmOnTheFly in Firm where Dependencies will see it. + # This is analogous to nesting models in a migration. + Firm.const_set :FirmOnTheFly, Class.new(Firm) - # And instantiate will find the existing constant rather than trying - # to require firm_on_the_fly. - assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } - ensure - ActiveRecord::Base.store_full_sti_class = true + # And instantiate will find the existing constant rather than trying + # to require firm_on_the_fly. + assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + end end def test_sti_type_from_attributes_disabled_in_non_sti_class diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 0021988083..018b7b0d8f 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'cases/helper' require 'models/company' diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 8416c81f45..6523fc29fd 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class TestAdapterWithInvalidConnection < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Bird < ActiveRecord::Base end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index ee43f07dd7..dbdcc84b7d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -177,6 +177,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_stale_object + person = Person.create!(first_name: 'Mehmet Emin') + stale_person = Person.find(person.id) + person.update_attribute(:gender, 'M') + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -215,10 +225,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new assert_equal 0, t1.custom_lock_version + assert_nil t1.custom_lock_version_before_type_cast - t1.save - t1 = LockWithCustomColumnWithoutDefault.find(t1.id) + t1.save! + t1.reload assert_equal 0, t1.custom_lock_version + assert [0, "0"].include?(t1.custom_lock_version_before_type_cast) end def test_readonly_attributes @@ -283,10 +295,10 @@ end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase fixtures :people, :legacy_things, :references - # need to disable transactional fixtures, because otherwise the sqlite3 + # need to disable transactional tests, because otherwise the sqlite3 # adapter (at least) chokes when we try and change the schema in the middle # of a test (see test_increment_counter_*). - self.use_transactional_fixtures = false + self.use_transactional_tests = false { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model| define_method("test_increment_counter_updates_#{name}") do @@ -363,7 +375,7 @@ end # (See exec vs. async_exec in the PostgreSQL adapter.) unless in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :people, :readers def setup diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index a578e81844..4192d12ff4 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -63,14 +63,6 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/ruby rails/, logger.debugs.first) end - def test_ignore_binds_payload_with_nil_column - event = Struct.new(:duration, :payload) - - logger = TestDebugLogSubscriber.new - logger.sql(event.new(0, sql: 'hi mom!', binds: [[nil, 1]])) - assert_equal 1, logger.debugs.length - end - def test_basic_query_logging Developer.all.load wait @@ -125,12 +117,5 @@ class LogSubscriberTest < ActiveRecord::TestCase wait assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) end - - def test_nil_binary_data_is_logged - binary = Binary.create(data: "") - binary.update_attributes(data: nil) - wait - assert_match(/<NULL binary data>/, @logger.logged(:debug).join) - end end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index b3129a8984..83e50048ec 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -68,8 +68,8 @@ module ActiveRecord five = columns.detect { |c| c.name == "five" } unless mysql assert_equal "hello", one.default - assert_equal true, two.type_cast_from_database(two.default) - assert_equal false, three.type_cast_from_database(three.default) + assert_equal true, connection.lookup_cast_type_from_column(two).deserialize(two.default) + assert_equal false, connection.lookup_cast_type_from_column(three).deserialize(three.default) assert_equal '1', four.default assert_equal "hello", five.default unless mysql end @@ -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 @@ -403,6 +403,17 @@ module ActiveRecord end end + def test_drop_table_if_exists + connection.create_table(:testings) + assert connection.table_exists?(:testings) + connection.drop_table(:testings, if_exists: true) + assert_not connection.table_exists?(:testings) + end + + def test_drop_table_if_exists_nothing_raised + assert_nothing_raised { connection.drop_table(:nonexistent, if_exists: true) } + end + private def testing_table_with_only_foo_attribute connection.create_table :testings, :id => false do |t| @@ -415,7 +426,7 @@ module ActiveRecord if ActiveRecord::Base.connection.supports_foreign_keys? class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @connection = ActiveRecord::Base.connection @@ -426,7 +437,7 @@ module ActiveRecord teardown do [:wagons, :trains].each do |table| - @connection.drop_table(table) if @connection.table_exists?(table) + @connection.drop_table table, if_exists: true end end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 7010af5434..2ffe7a1b0d 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end def with_change_table - yield ConnectionAdapters::Table.new(:delete_me, @connection) + yield ActiveRecord::Base.connection.update_table_definition(:delete_me, @connection) end def test_references_column_type_adds_id @@ -100,6 +100,13 @@ module ActiveRecord end end + def test_primary_key_creates_primary_key_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] + t.primary_key :id, first: true + end + end + def test_integer_creates_integer_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}] @@ -108,6 +115,14 @@ module ActiveRecord end end + def test_bigint_creates_bigint_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :bigint, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :bigint, {}] + t.bigint :foo, :bar + end + end + def test_string_creates_string_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :foo, :string, {}] @@ -116,6 +131,24 @@ module ActiveRecord end end + if current_adapter?(:PostgreSQLAdapter) + def test_json_creates_json_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :json, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :json, {}] + t.json :foo, :bar + end + end + + def test_xml_creates_xml_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :xml, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :xml, {}] + t.xml :foo, :bar + end + end + end + def test_column_creates_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 763aa88f72..8d8e661aa5 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ColumnAttributesTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_add_column_newline_default string = "foo\nbar" diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 62186e13a5..4637970ce0 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -3,7 +3,7 @@ require 'cases/helper' module ActiveRecord class Migration class ColumnPositioningTest < ActiveRecord::TestCase - attr_reader :connection, :table_name + attr_reader :connection alias :conn :connection def setup diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index e6aa901814..5fc7702dfa 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ColumnsTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false # FIXME: this is more of an integration test with AR::Base and the # schema modifications. Maybe we should move this? @@ -65,7 +65,7 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" - assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? TestModel.reset_column_information ensure rename_column "test_models", "id_test", "id" @@ -196,7 +196,7 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - default = c.type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' && c.type == :boolean && default == true } @@ -204,11 +204,11 @@ module ActiveRecord new_columns = connection.columns(TestModel.table_name) assert_not new_columns.find { |c| - default = c.type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' and c.type == :boolean and default == true } assert new_columns.find { |c| - default = c.type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' and c.type == :boolean and default == false } change_column :test_models, :approved, :boolean, :default => true diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 8cba777fe2..24d3c085a7 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -256,6 +256,11 @@ module ActiveRecord assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add end + def test_invert_remove_reference_with_index_and_foreign_key + add = @recorder.inverse_of :remove_reference, [:table, :taggable, { index: true, foreign_key: true }] + assert_equal [:add_reference, [:table, :taggable, { index: true, foreign_key: true }], nil], add + end + def test_invert_remove_belongs_to_alias add = @recorder.inverse_of :remove_belongs_to, [:table, :user] assert_equal [:add_reference, [:table, :user], nil], add @@ -276,17 +281,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 @@ -294,6 +324,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/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index bea9d6b2c9..8fd08fe4ce 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -140,7 +140,7 @@ module ActiveRecord tables_after = connection.tables - tables_before tables_after.each do |table| - connection.execute "DROP TABLE #{table}" + connection.drop_table table end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index ad35d690bd..7f4790bf3e 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -8,6 +8,7 @@ module ActiveRecord class ForeignKeyTest < ActiveRecord::TestCase include DdlHelper include SchemaDumpingHelper + include ActiveSupport::Testing::Stream class Rocket < ActiveRecord::Base end @@ -29,8 +30,8 @@ module ActiveRecord teardown do if defined?(@connection) - @connection.drop_table "astronauts" if @connection.table_exists? 'astronauts' - @connection.drop_table "rockets" if @connection.table_exists? 'rockets' + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true end end @@ -57,7 +58,7 @@ module ActiveRecord assert_equal "rockets", fk.to_table assert_equal "rocket_id", fk.column assert_equal "id", fk.primary_key - assert_match(/^fk_rails_.{10}$/, fk.name) + assert_equal("fk_rails_78146ddd2e", fk.name) end def test_add_foreign_key_with_column @@ -71,7 +72,7 @@ module ActiveRecord assert_equal "rockets", fk.to_table assert_equal "rocket_id", fk.column assert_equal "id", fk.primary_key - assert_match(/^fk_rails_.{10}$/, fk.name) + assert_equal("fk_rails_78146ddd2e", fk.name) end def test_add_foreign_key_with_non_standard_primary_key @@ -146,6 +147,27 @@ module ActiveRecord assert_equal :nullify, fk.on_update end + def test_foreign_key_exists + @connection.add_foreign_key :astronauts, :rockets + + assert @connection.foreign_key_exists?(:astronauts, :rockets) + assert_not @connection.foreign_key_exists?(:astronauts, :stars) + end + + def test_foreign_key_exists_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id") + assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id") + end + + def test_foreign_key_exists_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk") + assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk") + end + def test_remove_foreign_key_inferes_column @connection.add_foreign_key :astronauts, :rockets @@ -221,17 +243,6 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end - private - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end end end diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 319d3e1af3..bf6e684887 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -4,7 +4,7 @@ module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase # MySQL can't roll back ddl changes - self.use_transactional_fixtures = false + self.use_transactional_tests = false Migration = Struct.new(:name, :version) do def disable_ddl_transaction; false end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index bba8897a0d..1594f99852 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -10,8 +10,8 @@ module ActiveRecord end teardown do - @connection.execute("drop table if exists testings") - @connection.execute("drop table if exists testing_parents") + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true end test "foreign keys can be created with the table" do @@ -95,7 +95,59 @@ module ActiveRecord end end end + + test "foreign key column can be removed" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :testing_parent, foreign_key: true + end + end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end + end + end +end +else +class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table("testings", if_exists: true) + @connection.drop_table("testing_parents", if_exists: true) + end + + test "ignores foreign keys defined with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true end + + assert_includes @connection.tables, "testings" end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 988bd9c89f..f613fd66c3 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ReferencesStatementsTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup super diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index a018bac43d..6d742d3f2f 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class RenameTableTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup super @@ -86,8 +86,8 @@ module ActiveRecord assert connection.table_exists? :felines ensure disable_extension!('uuid-ossp', connection) - connection.drop_table :cats if connection.table_exists? :cats - connection.drop_table :felines if connection.table_exists? :felines + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true end end end diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb index 8fd770abd1..24cba84a09 100644 --- a/activerecord/test/cases/migration/table_and_index_test.rb +++ b/activerecord/test/cases/migration/table_and_index_test.rb @@ -6,11 +6,11 @@ module ActiveRecord def test_add_schema_info_respects_prefix_and_suffix conn = ActiveRecord::Base.connection - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters ActiveRecord::Base.table_name_prefix = 'p_' ActiveRecord::Base.table_name_suffix = '_s' - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) conn.initialize_schema_migrations_table diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5829ef2100..b2f209fe97 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -14,9 +14,9 @@ require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" class BigNumber < ActiveRecord::Base unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) - attribute :value_of_e, Type::Integer.new + attribute :value_of_e, :integer end - attribute :my_house_population, Type::Integer.new + attribute :my_house_population, :integer end class Reminder < ActiveRecord::Base; end @@ -24,7 +24,7 @@ class Reminder < ActiveRecord::Base; end class Thing < ActiveRecord::Base; end class MigrationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :people @@ -90,7 +90,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_migration_detection_without_schema_migration_table - ActiveRecord::Base.connection.drop_table('schema_migrations') if ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveRecord::Base.connection.drop_table 'schema_migrations', if_exists: true migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths @@ -119,10 +119,6 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_force_true_does_not_drop_nonexisting_table - if Person.connection.table_exists?(:testings2) - Person.connection.drop_table :testings2 - end - # using a copy as we need the drop_table method to # continue to work for the ensure block of the test temp_conn = Person.connection.dup @@ -133,7 +129,7 @@ class MigrationTest < ActiveRecord::TestCase t.column :foo, :string end ensure - Person.connection.drop_table :testings2 rescue nil + Person.connection.drop_table :testings2, if_exists: true end def connection @@ -162,6 +158,7 @@ class MigrationTest < ActiveRecord::TestCase assert !BigNumber.table_exists? GiveMeBigNumbers.up + BigNumber.reset_column_information assert BigNumber.create( :bank_balance => 1586.43, @@ -397,6 +394,7 @@ class MigrationTest < ActiveRecord::TestCase Thing.reset_table_name Thing.reset_sequence_name WeNeedThings.up + Thing.reset_column_information assert Thing.create("content" => "hello world") assert_equal "hello world", Thing.first.content @@ -416,6 +414,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = '_suffix' Reminder.reset_table_name Reminder.reset_sequence_name + Reminder.reset_column_information WeNeedReminders.up assert Reminder.create("content" => "hello world", "remind_at" => Time.now) assert_equal "hello world", Reminder.first.content @@ -427,8 +426,6 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_binary_column - Person.connection.drop_table :binary_testings rescue nil - assert_nothing_raised { Person.connection.create_table :binary_testings do |t| t.column "data", :binary, :null => false @@ -440,7 +437,7 @@ class MigrationTest < ActiveRecord::TestCase assert_nil data_column.default - Person.connection.drop_table :binary_testings rescue nil + Person.connection.drop_table :binary_testings, if_exists: true end unless mysql_enforcing_gtid_consistency? @@ -511,12 +508,14 @@ class MigrationTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise Person.connection.drop_table :test_limits rescue nil - assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do Person.connection.create_table :test_integer_limits, :force => true do |t| t.column :bigone, :integer, :limit => 10 end end + assert_match(/No integer type has byte size 10/, e.message) + unless current_adapter?(:PostgreSQLAdapter) assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do Person.connection.create_table :test_text_limits, :force => true do |t| @@ -718,6 +717,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end class CopyMigrationsTest < ActiveRecord::TestCase + include ActiveSupport::Testing::Stream + def setup end @@ -927,23 +928,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = old end - private - - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index c0daa83e9c..2ff6938e7b 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require "cases/migration/helper" class MigratorTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false # Use this class to sense if migrations have gone # up or down. @@ -312,7 +312,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_db_has_no_schema_migrations_table _, migrator = migrator_class(3) - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') migrator.migrate("valid", 1) assert ActiveRecord::Base.connection.table_exists?('schema_migrations') diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index 7ddb2bfee1..7ebdcac711 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -61,8 +61,6 @@ class TouchTest < ActiveRecord::TestCase # Make sure Mixin.record_timestamps gets reset, even if this test fails, # so that other tests do not fail because Mixin.record_timestamps == false - rescue Exception => e - raise e ensure Mixin.record_timestamps = true end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 6f65bf80eb..7f31325f47 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -68,8 +68,7 @@ class ModulesTest < ActiveRecord::TestCase end end - # need to add an eager loading condition to force the eager loading model into - # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640 + # An eager loading condition to force the eager loading model into the old join model. def test_eager_loading_in_modules clients = [] diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index 14d4ef457d..ae18573126 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -199,6 +199,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.reset_column_information attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" @@ -209,6 +210,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time assert_equal Time.zone, topic.written_on.time_zone end + ensure + Topic.reset_column_information end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false @@ -227,6 +230,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do Topic.skip_time_zone_conversion_for_attributes = [:written_on] + Topic.reset_column_information attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" @@ -238,21 +242,25 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end ensure Topic.skip_time_zone_conversion_for_attributes = [] + Topic.reset_column_information end # Oracle does not have a TIME datatype. unless current_adapter?(:OracleAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.reset_column_information attributes = { "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert topic.bonus_time.utc? + assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert_not topic.bonus_time.utc? end + ensure + Topic.reset_column_information end end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 3831de6ae3..39cdcf5403 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -4,7 +4,7 @@ require 'models/bird' require 'models/course' class MultipleDbTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @courses = create_fixtures("courses") { Course.retrieve_connection } @@ -94,6 +94,13 @@ class MultipleDbTest < ActiveRecord::TestCase end unless in_memory_db? + def test_count_on_custom_connection + ActiveRecord::Base.remove_connection + assert_equal 1, College.count + ensure + ActiveRecord::Base.establish_connection :arunit + end + def test_associations_should_work_when_model_has_no_connection begin ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 5c7e8a65d2..6b4addd52f 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -943,7 +943,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase end class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup @pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!") @@ -983,7 +983,7 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe end class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup @ship = Ship.create!(:name => "The good ship Dollypop") @@ -1037,4 +1037,21 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR ShipPart.create!(:ship => @ship, :name => "Stern") assert_no_queries { @ship.valid? } end + + test "circular references do not perform unnecessary queries" do + ship = Ship.new(name: "The Black Rock") + part = ship.parts.build(name: "Stern") + ship.treasures.build(looter: part) + + assert_queries 3 do + ship.save! + end + end + + test "nested singular associations are validated" do + part = ShipPart.new(name: "Stern", ship_attributes: { name: nil }) + + assert_not part.valid? + assert_equal ["Ship name can't be blank"], part.errors.full_messages + end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index d6816041bc..42e7507631 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -356,6 +356,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal("David", topic_reloaded.author_name) end + def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed + klass = Class.new(Topic) do + def self.name; 'Topic'; end + end + topic = klass.create(title: 'Another New Topic') + assert_queries(0) do + topic.update_attribute(:title, 'Another New Topic') + end + end + + def test_update_does_not_run_sql_if_record_has_not_changed + topic = Topic.create(title: 'Another New Topic') + assert_queries(0) { topic.update(title: 'Another New Topic') } + assert_queries(0) { topic.update_attributes(title: 'Another New Topic') } + end + def test_delete topic = Topic.find(1) assert_equal topic, topic.delete, 'topic.delete did not return self' @@ -881,8 +897,35 @@ class PersistenceTest < ActiveRecord::TestCase assert_not post.new_record? end + def test_reload_via_querycache + ActiveRecord::Base.connection.enable_query_cache! + ActiveRecord::Base.connection.clear_query_cache + assert ActiveRecord::Base.connection.query_cache_enabled, 'cache should be on' + parrot = Parrot.create(:name => 'Shane') + + # populate the cache with the SELECT result + found_parrot = Parrot.find(parrot.id) + assert_equal parrot.id, found_parrot.id + + # Manually update the 'name' attribute in the DB directly + assert_equal 1, ActiveRecord::Base.connection.query_cache.length + ActiveRecord::Base.uncached do + found_parrot.name = 'Mary' + found_parrot.save + end + + # Now reload, and verify that it gets the DB version, and not the querycache version + found_parrot.reload + assert_equal 'Mary', found_parrot.name + + found_parrot = Parrot.find(parrot.id) + assert_equal 'Mary', found_parrot.name + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + class SaveTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_save_touch_false widget = Class.new(ActiveRecord::Base) do @@ -908,7 +951,8 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 287a3f33ea..daa3271777 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -3,7 +3,7 @@ require "models/project" require "timeout" class PooledConnectionsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @per_test_teardown = [] diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 751eccc015..b8433f0bba 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -175,10 +175,18 @@ class PrimaryKeysTest < ActiveRecord::TestCase dashboard = Dashboard.first assert_equal '2', dashboard.id end + + if current_adapter?(:PostgreSQLAdapter) + def test_serial_with_quoted_sequence_name + column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] + assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function + assert column.serial? + end + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false unless in_memory_db? def test_set_primary_key_with_no_connection @@ -199,7 +207,7 @@ end class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Barcode < ActiveRecord::Base end @@ -210,7 +218,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.execute("DROP TABLE IF EXISTS barcodes") + @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes end def test_any_type_primary_key @@ -229,7 +237,7 @@ end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_primary_key_method_with_ansi_quotes con = ActiveRecord::Base.connection @@ -245,7 +253,7 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Widget < ActiveRecord::Base end @@ -260,7 +268,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.execute("DROP TABLE IF EXISTS widgets") + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do @@ -282,5 +291,15 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) assert_match %r{create_table "widgets", id: :bigint}, schema end end + + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + test "primary key column type with options" do + @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true) + column = @connection.columns(:widgets).find { |c| c.name == 'id' } + assert column.auto_increment? + assert_equal :integer, column.type + assert_equal 8, column.limit + end + end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 744f9edc47..2f0b5df286 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -184,7 +184,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 1d6ae2f67f..6d91f96bf6 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -46,28 +46,28 @@ module ActiveRecord def test_quoted_time_utc with_timezone_config default: :utc do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_local with_timezone_config default: :local do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_crazy with_timezone_config default: :asdfasdf do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_datetime_utc with_timezone_config default: :utc do - t = DateTime.now + t = Time.now.change(usec: 0).to_datetime assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end @@ -76,7 +76,7 @@ module ActiveRecord # DateTime doesn't define getlocal, so make sure it does nothing def test_quoted_datetime_local with_timezone_config default: :local do - t = DateTime.now + t = Time.now.change(usec: 0).to_datetime assert_equal t.to_s(:db), @quoter.quoted_date(t) end end @@ -125,14 +125,11 @@ module ActiveRecord end def test_crazy_object - crazy = Class.new.new - expected = "'#{YAML.dump(crazy)}'" - assert_equal expected, @quoter.quote(crazy, nil) - end - - def test_crazy_object_calls_quote_string - crazy = Class.new { def initialize; @lol = 'lo\l' end }.new - assert_match "lo\\\\l", @quoter.quote(crazy, nil) + crazy = Object.new + e = assert_raises(TypeError) do + @quoter.quote(crazy, nil) + end + assert_equal "can't quote Object", e.message end def test_quote_string_no_column diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index a2252a836f..7b47c80331 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -23,6 +23,7 @@ require 'models/chef' require 'models/department' require 'models/cake_designer' require 'models/drink_designer' +require 'models/recipe' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -86,18 +87,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "attribute_that_doesnt_exist", column.name assert_equal nil, column.sql_type assert_equal nil, column.type - assert_not column.number? - assert_not column.text? - assert_not column.binary? end - def test_non_existent_columns_are_identity_types - column = @first.column_for_attribute("attribute_that_doesnt_exist") + def test_non_existent_types_are_identity_types + type = @first.type_for_attribute("attribute_that_doesnt_exist") object = Object.new - assert_equal object, column.type_cast_from_database(object) - assert_equal object, column.type_cast_from_user(object) - assert_equal object, column.type_cast_for_database(object) + assert_equal object, type.deserialize(object) + assert_equal object, type.cast(object) + assert_equal object, type.serialize(object) end def test_reflection_klass_for_nested_class_name @@ -280,6 +278,22 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal 2, @hotel.chefs.size end + def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations + hotel = Hotel.create! + department = hotel.departments.create! + drink = department.chefs.create!(employable: DrinkDesigner.create!) + Recipe.create!(chef_id: drink.id, hotel_id: hotel.id) + + expected_sql = capture_sql { hotel.recipes.to_a } + + Hotel.reflect_on_association(:recipes).clear_association_scope_cache + hotel.reload + hotel.drink_designers.to_a + loaded_sql = capture_sql { hotel.recipes.to_a } + + assert_equal expected_sql, loaded_sql + end + def test_nested? assert !Author.reflect_on_association(:comments).nested? assert Author.reflect_on_association(:tags).nested? diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 29c9d0e2af..989f4e1e5d 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -28,7 +28,7 @@ module ActiveRecord module DelegationWhitelistBlacklistTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], - :all?, :collect, :detect, :each, :each_cons, :each_with_index, + :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index eb76ef6328..0a2e874e4f 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -82,26 +82,15 @@ class RelationMergingTest < ActiveRecord::TestCase left = Post.where(title: "omg").where(comments_count: 1) right = Post.where(title: "wtf").where(title: "bbq") - expected = [left.bind_values[1]] + right.bind_values + expected = [left.bound_attributes[1]] + right.bound_attributes merged = left.merge(right) - assert_equal expected, merged.bind_values + assert_equal expected, merged.bound_attributes assert !merged.to_sql.include?("omg") assert merged.to_sql.include?("wtf") assert merged.to_sql.include?("bbq") end - def test_merging_keeps_lhs_bind_parameters - column = Post.columns_hash['id'] - binds = [[column, 20]] - - right = Post.where(id: 20) - left = Post.where(id: 10) - - merged = left.merge(right) - assert_equal binds, merged.bind_values - end - def test_merging_reorders_bind_params post = Post.first right = Post.where(id: 1) diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 2443f10269..ba4d9d2503 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -81,7 +81,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -90,7 +90,7 @@ module ActiveRecord test '#from!' do assert relation.from!('foo').equal?(relation) - assert_equal ['foo', nil], relation.from_value + assert_equal 'foo', relation.from_clause.value end test '#lock!' do @@ -136,12 +136,12 @@ module ActiveRecord end test 'test_merge!' do - assert relation.merge!(where: :foo).equal?(relation) - assert_equal [:foo], relation.where_values + assert relation.merge!(select: :foo).equal?(relation) + assert_equal [:foo], relation.select_values end test 'merge with a proc' do - assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values + assert_equal [:foo], relation.merge(-> { select(:foo) }).select_values end test 'none!' do @@ -153,13 +153,22 @@ module ActiveRecord test 'distinct!' do relation.distinct! :foo assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access + + assert_deprecated do + assert_equal :foo, relation.uniq_value # deprecated access + end end test 'uniq! was replaced by distinct!' do - relation.uniq! :foo + assert_deprecated(/use distinct! instead/) do + relation.uniq! :foo + end + + assert_deprecated(/use distinct_value instead/) do + assert_equal :foo, relation.uniq_value # deprecated access + end + assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb new file mode 100644 index 0000000000..2006fc9611 --- /dev/null +++ b/activerecord/test/cases/relation/or_test.rb @@ -0,0 +1,84 @@ +require "cases/helper" +require 'models/post' + +module ActiveRecord + class OrTest < ActiveRecord::TestCase + fixtures :posts + + def test_or_with_relation + expected = Post.where('id = 1 or id = 2').to_a + assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a + end + + def test_or_identity + expected = Post.where('id = 1').to_a + assert_equal expected, Post.where('id = 1').or(Post.where('id = 1')).to_a + end + + def test_or_with_null_left + expected = Post.where('id = 1').to_a + assert_equal expected, Post.none.or(Post.where('id = 1')).to_a + end + + def test_or_with_null_right + expected = Post.where('id = 1').to_a + assert_equal expected, Post.where('id = 1').or(Post.none).to_a + end + + def test_or_with_bind_params + assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).to_a + end + + def test_or_with_null_both + expected = Post.none.to_a + assert_equal expected, Post.none.or(Post.none).to_a + end + + def test_or_without_left_where + expected = Post.all + assert_equal expected, Post.or(Post.where('id = 1')).to_a + end + + def test_or_without_right_where + expected = Post.all + assert_equal expected, Post.where('id = 1').or(Post.all).to_a + end + + def test_or_preserves_other_querying_methods + expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a + partial = Post.order('body asc') + assert_equal expected, partial.where('id = 1').or(partial.where(:id => [2, 3])).to_a + assert_equal expected, Post.order('body asc').where('id = 1').or(Post.order('body asc').where(:id => [2, 3])).to_a + end + + def test_or_with_incompatible_relations + assert_raises ArgumentError do + Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a + end + end + + def test_or_when_grouping + groups = Post.where('id < 10').group('body').select('body, COUNT(*) AS c') + expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] } + assert_equal expected, groups.having('COUNT(*) > 1').or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] } + end + + def test_or_with_named_scope + expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a + assert_equal expected, Post.where('id = 1').or(Post.containing_the_letter_a) + end + + def test_or_inside_named_scope + expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order('id DESC').to_a + assert_equal expected, Post.order(id: :desc).typographically_interesting + end + + def test_or_on_loaded_relation + expected = Post.where('id = 1 or id = 2').to_a + p = Post.where('id = 1') + p.load + assert_equal p.loaded?, true + assert_equal expected, p.or(Post.where('id = 2')).to_a + end + end +end diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 0cc081fced..8f62014622 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -8,7 +8,7 @@ module ActiveRecord Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) - assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql + assert_match %r{["`]topics["`]\.["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql ensure Topic.reset_column_information end diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb new file mode 100644 index 0000000000..62f0a7cc49 --- /dev/null +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -0,0 +1,28 @@ +require 'cases/helper' +require 'models/post' + +module ActiveRecord + class RecordFetchWarningTest < ActiveRecord::TestCase + fixtures :posts + + def test_warn_on_records_fetched_greater_than + original_logger = ActiveRecord::Base.logger + orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + + log = StringIO.new + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::WARN + + require 'active_record/relation/record_fetch_warning' + + ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + + Post.all.to_a + + assert_match(/Query fetched/, log.string) + ensure + ActiveRecord::Base.logger = original_logger + ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than + end + end +end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 619055f1e7..27bbd80f79 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -11,22 +11,11 @@ module ActiveRecord @name = 'title' end - def test_not_eq + def test_not_inverts_where_clause relation = Post.where.not(title: 'hello') + expected_where_clause = Post.where(title: 'hello').where_clause.invert - assert_equal 1, relation.where_values.length - - value = relation.where_values.first - bind = relation.bind_values.first - - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual - assert_equal 'hello', bind.last - end - - def test_not_null - expected = Post.arel_table[@name].not_eq(nil) - relation = Post.where.not(title: nil) - assert_equal([expected], relation.where_values) + assert_equal expected_where_clause, relation.where_clause end def test_not_with_nil @@ -35,146 +24,81 @@ module ActiveRecord end end - def test_not_in - expected = Post.arel_table[@name].not_in(%w[hello goodbye]) - relation = Post.where.not(title: %w[hello goodbye]) - assert_equal([expected], relation.where_values) - end - def test_association_not_eq - expected = Comment.arel_table[@name].not_eq('hello') + expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new)) relation = Post.joins(:comments).where.not(comments: {title: 'hello'}) - assert_equal(expected.to_sql, relation.where_values.first.to_sql) + assert_equal(expected.to_sql, relation.where_clause.ast.to_sql) end def test_not_eq_with_preceding_where relation = Post.where(title: 'hello').where.not(title: 'world') + expected_where_clause = + Post.where(title: 'hello').where_clause + + Post.where(title: 'world').where_clause.invert - value = relation.where_values.first - bind = relation.bind_values.first - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality - assert_equal 'hello', bind.last - - value = relation.where_values.last - bind = relation.bind_values.last - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual - assert_equal 'world', bind.last + assert_equal expected_where_clause, relation.where_clause end def test_not_eq_with_succeeding_where relation = Post.where.not(title: 'hello').where(title: 'world') + expected_where_clause = + Post.where(title: 'hello').where_clause.invert + + Post.where(title: 'world').where_clause - value = relation.where_values.first - bind = relation.bind_values.first - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual - assert_equal 'hello', bind.last - - value = relation.where_values.last - bind = relation.bind_values.last - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality - assert_equal 'world', bind.last - end - - def test_not_eq_with_string_parameter - expected = Arel::Nodes::Not.new("title = 'hello'") - relation = Post.where.not("title = 'hello'") - assert_equal([expected], relation.where_values) - end - - def test_not_eq_with_array_parameter - expected = Arel::Nodes::Not.new("title = 'hello'") - relation = Post.where.not(['title = ?', 'hello']) - assert_equal([expected], relation.where_values) + assert_equal expected_where_clause, relation.where_clause end def test_chaining_multiple relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails') + expected_where_clause = + Post.where(author_id: [1, 2]).where_clause.invert + + Post.where(title: 'ruby on rails').where_clause.invert - expected = Post.arel_table['author_id'].not_in([1, 2]) - assert_equal(expected, relation.where_values[0]) - - value = relation.where_values[1] - bind = relation.bind_values.first - - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual - assert_equal 'ruby on rails', bind.last + assert_equal expected_where_clause, relation.where_clause end def test_rewhere_with_one_condition relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') + expected = Post.where(title: 'alone') - assert_equal 1, relation.where_values.size - value = relation.where_values.first - bind = relation.bind_values.first - assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality - assert_equal 'alone', bind.last + assert_equal expected.where_clause, relation.where_clause end def test_rewhere_with_multiple_overwriting_conditions relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again') + expected = Post.where(title: 'alone', body: 'again') - assert_equal 2, relation.where_values.size - - value = relation.where_values.first - bind = relation.bind_values.first - assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality - assert_equal 'alone', bind.last - - value = relation.where_values[1] - bind = relation.bind_values[1] - assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality - assert_equal 'again', bind.last - end - - def assert_bound_ast value, table, type - assert_equal table, value.left - assert_kind_of type, value - assert_kind_of Arel::Nodes::BindParam, value.right + assert_equal expected.where_clause, relation.where_clause end def test_rewhere_with_one_overwriting_condition_and_one_unrelated relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') + expected = Post.where(body: 'world', title: 'alone') - assert_equal 2, relation.where_values.size - - value = relation.where_values.first - bind = relation.bind_values.first - - assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality - assert_equal 'world', bind.last - - value = relation.where_values.second - bind = relation.bind_values.second - - assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality - assert_equal 'alone', bind.last + assert_equal expected.where_clause, relation.where_clause end def test_rewhere_with_range relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5) - assert_equal 1, relation.where_values.size assert_equal Post.where(comments_count: 3..5), relation end def test_rewhere_with_infinite_upper_bound_range relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5) - assert_equal 1, relation.where_values.size assert_equal Post.where(comments_count: 3..5), relation end def test_rewhere_with_infinite_lower_bound_range relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5) - assert_equal 1, relation.where_values.size assert_equal Post.where(comments_count: 3..5), relation end def test_rewhere_with_infinite_range relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5) - assert_equal 1, relation.where_values.size assert_equal Post.where(comments_count: 3..5), relation end end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb new file mode 100644 index 0000000000..c20ed94d90 --- /dev/null +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -0,0 +1,182 @@ +require "cases/helper" + +class ActiveRecord::Relation + class WhereClauseTest < ActiveRecord::TestCase + test "+ combines two where clauses" do + first_clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]]) + second_clause = WhereClause.new([table["name"].eq(bind_param)], [["name", "Sean"]]) + combined = WhereClause.new( + [table["id"].eq(bind_param), table["name"].eq(bind_param)], + [["id", 1], ["name", "Sean"]], + ) + + assert_equal combined, first_clause + second_clause + end + + test "+ is associative, but not commutative" do + a = WhereClause.new(["a"], ["bind a"]) + b = WhereClause.new(["b"], ["bind b"]) + c = WhereClause.new(["c"], ["bind c"]) + + assert_equal a + (b + c), (a + b) + c + assert_not_equal a + b, b + a + end + + test "an empty where clause is the identity value for +" do + clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]]) + + assert_equal clause, clause + WhereClause.empty + end + + test "merge combines two where clauses" do + a = WhereClause.new([table["id"].eq(1)], []) + b = WhereClause.new([table["name"].eq("Sean")], []) + expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], []) + + assert_equal expected, a.merge(b) + end + + test "merge keeps the right side, when two equality clauses reference the same column" do + a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], []) + b = WhereClause.new([table["name"].eq("Jim")], []) + expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")], []) + + assert_equal expected, a.merge(b) + end + + test "merge removes bind parameters matching overlapping equality clauses" do + a = WhereClause.new( + [table["id"].eq(bind_param), table["name"].eq(bind_param)], + [attribute("id", 1), attribute("name", "Sean")], + ) + b = WhereClause.new( + [table["name"].eq(bind_param)], + [attribute("name", "Jim")] + ) + expected = WhereClause.new( + [table["id"].eq(bind_param), table["name"].eq(bind_param)], + [attribute("id", 1), attribute("name", "Jim")], + ) + + assert_equal expected, a.merge(b) + end + + test "merge allows for columns with the same name from different tables" do + skip "This is not possible as of 4.2, and the binds do not yet contain sufficient information for this to happen" + # We might be able to change the implementation to remove conflicts by index, rather than column name + end + + test "a clause knows if it is empty" do + assert WhereClause.empty.empty? + assert_not WhereClause.new(["anything"], []).empty? + end + + test "invert cannot handle nil" do + where_clause = WhereClause.new([nil], []) + + assert_raises ArgumentError do + where_clause.invert + end + end + + test "invert replaces each part of the predicate with its inverse" do + random_object = Object.new + original = WhereClause.new([ + table["id"].in([1, 2, 3]), + table["id"].eq(1), + "sql literal", + random_object + ], []) + expected = WhereClause.new([ + table["id"].not_in([1, 2, 3]), + table["id"].not_eq(1), + Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")), + Arel::Nodes::Not.new(random_object) + ], []) + + assert_equal expected, original.invert + end + + test "accept removes binary predicates referencing a given column" do + where_clause = WhereClause.new([ + table["id"].in([1, 2, 3]), + table["name"].eq(bind_param), + table["age"].gteq(bind_param), + ], [ + attribute("name", "Sean"), + attribute("age", 30), + ]) + expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)]) + + assert_equal expected, where_clause.except("id", "name") + end + + test "ast groups its predicates with AND" do + predicates = [ + table["id"].in([1, 2, 3]), + table["name"].eq(bind_param), + ] + where_clause = WhereClause.new(predicates, []) + expected = Arel::Nodes::And.new(predicates) + + assert_equal expected, where_clause.ast + end + + test "ast wraps any SQL literals in parenthesis" do + random_object = Object.new + where_clause = WhereClause.new([ + table["id"].in([1, 2, 3]), + "foo = bar", + random_object, + ], []) + expected = Arel::Nodes::And.new([ + table["id"].in([1, 2, 3]), + Arel::Nodes::Grouping.new(Arel.sql("foo = bar")), + Arel::Nodes::Grouping.new(random_object), + ]) + + assert_equal expected, where_clause.ast + end + + test "ast removes any empty strings" do + where_clause = WhereClause.new([table["id"].in([1, 2, 3])], []) + where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ''], []) + + assert_equal where_clause.ast, where_clause_with_empty.ast + end + + test "or joins the two clauses using OR" do + where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) + other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")]) + expected_ast = + Arel::Nodes::Grouping.new( + Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param)) + ) + expected_binds = where_clause.binds + other_clause.binds + + assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql + assert_equal expected_binds, where_clause.or(other_clause).binds + end + + test "or returns an empty where clause when either side is empty" do + where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) + + assert_equal WhereClause.empty, where_clause.or(WhereClause.empty) + assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) + end + + private + + def table + Arel::Table.new("table") + end + + def bind_param + Arel::Nodes::BindParam.new + end + + def attribute(name, value) + ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new) + end + end +end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index d675953da6..6af31017d6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -1,17 +1,20 @@ require "cases/helper" -require 'models/author' -require 'models/price_estimate' -require 'models/treasure' -require 'models/post' -require 'models/comment' -require 'models/edge' -require 'models/topic' -require 'models/binary' -require 'models/vertex' +require "models/author" +require "models/binary" +require "models/cake_designer" +require "models/chef" +require "models/comment" +require "models/edge" +require "models/essay" +require "models/post" +require "models/price_estimate" +require "models/topic" +require "models/treasure" +require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries + fixtures :posts, :edges, :authors, :binaries, :essays def test_where_copies_bind_params author = authors(:david) @@ -26,6 +29,24 @@ module ActiveRecord } end + def test_where_copies_bind_params_in_the_right_order + author = authors(:david) + posts = author.posts.where.not(id: 1) + joined = Post.where(id: posts, title: posts.first.title) + + assert_equal joined, [posts.first] + end + + def test_where_copies_arel_bind_params + chef = Chef.create! + CakeDesigner.create!(chef: chef) + + cake_designers = CakeDesigner.joins(:chef).where(chefs: { id: chef.id }) + chefs = Chef.where(employable: cake_designers) + + assert_equal [chef], chefs.to_a + end + def test_rewhere_on_root assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first end @@ -220,5 +241,40 @@ module ActiveRecord count = Binary.where(:data => 0).count assert_equal 0, count end + + def test_where_on_association_with_custom_primary_key + author = authors(:david) + essay = Essay.where(writer: author).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_custom_primary_key_with_relation + author = authors(:david) + essay = Essay.where(writer: Author.where(id: author.id)).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_relation_performs_subselect_not_two_queries + author = authors(:david) + + assert_queries(1) do + Essay.where(writer: Author.where(id: author.id)).to_a + end + end + + def test_where_on_association_with_custom_primary_key_with_array_of_base + author = authors(:david) + essay = Essay.where(writer: [author]).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_custom_primary_key_with_array_of_ids + essay = Essay.where(writer: ["David"]).first + + assert_equal essays(:david_modest_proposal), essay + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index f7cb471984..37d3965022 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -156,35 +156,45 @@ module ActiveRecord relation = Relation.new(FakeKlass, :b, nil) relation = relation.merge where: :lol, readonly: true - assert_equal [:lol], relation.where_values + assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause assert_equal true, relation.readonly_value end test 'merging an empty hash into a relation' do - assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values + assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause end test 'merging a hash with unknown keys raises' do assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') } end + test 'merging nil or false raises' do + relation = Relation.new(FakeKlass, :b, nil) + + e = assert_raises(ArgumentError) do + relation = relation.merge nil + end + + assert_equal 'invalid argument: nil.', e.message + + e = assert_raises(ArgumentError) do + relation = relation.merge false + end + + assert_equal 'invalid argument: false.', e.message + end + test '#values returns a dup of the values' do relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values values[:where] = nil - assert_not_nil relation.where_values + assert_not_nil relation.where_clause end test 'relations can be created with a values hash' do - relation = Relation.new(FakeKlass, :b, nil, where: [:foo]) - assert_equal [:foo], relation.where_values - end - - test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b, nil) - relation.merge!(where: :foo) - assert_equal [:foo], relation.where_values + relation = Relation.new(FakeKlass, :b, nil, select: [:foo]) + assert_equal [:foo], relation.select_values end test 'merging a hash interpolates conditions' do @@ -197,7 +207,7 @@ module ActiveRecord relation = Relation.new(klass, :b, nil) relation.merge!(where: ['foo = ?', 'bar']) - assert_equal ['foo = bar'], relation.where_values + assert_equal Relation::WhereClause.new(['foo = bar'], []), relation.where_clause end def test_merging_readonly_false @@ -232,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 @@ -244,12 +267,12 @@ module ActiveRecord :string end - def type_cast_from_database(value) + def deserialize(value) raise value unless value == "type cast for database" "type cast from database" end - def type_cast_for_database(value) + def serialize(value) raise value unless value == "value from user" "type cast for database" end @@ -266,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 9631ea79be..acbf85d398 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -16,7 +16,8 @@ require 'models/engine' require 'models/tyre' require 'models/minivan' require 'models/aircraft' - +require "models/possession" +require "models/reader" class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, @@ -39,15 +40,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal van, Minivan.where(:minivan_id => [van]).to_a.first end - def test_bind_values - relation = Post.all - assert_equal [], relation.bind_values - - relation2 = relation.bind 'foo' - assert_equal %w{ foo }, relation2.bind_values - assert_equal [], relation.bind_values - end - def test_two_scopes_with_includes_should_not_drop_any_include # heat habtm cache car = Car.incl_engines.incl_tyres.first @@ -165,6 +157,17 @@ class RelationTest < ActiveRecord::TestCase end end + def test_select_with_subquery_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select('COUNT(post_id) AS post_count, type') + subquery = Comment.from(relation).select('type','post_count') + assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort) + end + + def test_group_with_subquery_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select('COUNT(post_id) AS post_count,type') + subquery = Comment.from(relation).group('type').average("post_count") + assert_equal(relation.map(&:post_count).sort,subquery.values.sort) + end def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) @@ -350,7 +353,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal 0, Developer.none.size assert_equal 0, Developer.none.count assert_equal true, Developer.none.empty? + assert_equal true, Developer.none.none? assert_equal false, Developer.none.any? + assert_equal false, Developer.none.one? assert_equal false, Developer.none.many? end end @@ -616,6 +621,32 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, query.to_a.size end + def test_preloading_with_associations_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + comment = Comment.create! post_id: post.id, body: 'body' + + assert !comment.respond_to?(:readers) + + post_rel = Post.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).to_a.first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + + post_rel = Post.includes(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } @@ -857,6 +888,12 @@ class RelationTest < ActiveRecord::TestCase assert ! fake.exists?(authors(:david).id) end + def test_exists_uses_existing_scope + post = authors(:david).posts.first + authors = Author.includes(:posts).where(name: "David", posts: { id: post.id }) + assert authors.exists?(authors(:david).id) + end + def test_last authors = Author.all assert_equal authors(:bob), authors.last @@ -897,7 +934,7 @@ class RelationTest < ActiveRecord::TestCase def test_delete_all_with_unpermitted_relation_raises_error assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } @@ -1104,6 +1141,38 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.limit(1).many? end + def test_none? + posts = Post.all + assert_queries(1) do + assert ! posts.none? # Uses COUNT() + end + + assert ! posts.loaded? + + assert_queries(1) do + assert posts.none? {|p| p.id < 0 } + assert ! posts.none? {|p| p.id == 1 } + end + + assert posts.loaded? + end + + def test_one + posts = Post.all + assert_queries(1) do + assert ! posts.one? # Uses COUNT() + end + + assert ! posts.loaded? + + assert_queries(1) do + assert ! posts.one? {|p| p.id < 3 } + assert posts.one? {|p| p.id == 1 } + end + + assert posts.loaded? + end + def test_build posts = Post.all @@ -1450,22 +1519,46 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct.map(&:name) - assert_equal ['Foo'], query.uniq.map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq.map(&:name) } end assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct(true).map(&:name) - assert_equal ['Foo'], query.uniq(true).map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq(true).map(&:name) } end assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) - assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + + assert_deprecated do + assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + end end def test_doesnt_add_having_values_if_options_are_blank scope = Post.having('') - assert_equal [], scope.having_values + assert scope.having_clause.empty? scope = Post.having([]) - assert_equal [], scope.having_values + assert scope.having_clause.empty? + end + + def test_having_with_binds_for_both_where_and_having + post = Post.first + having_then_where = Post.having(id: post.id).where(title: post.title).group(:id) + where_then_having = Post.where(title: post.title).having(id: post.id).group(:id) + + assert_equal [post], having_then_where + assert_equal [post], where_then_having + end + + def test_multiple_where_and_having_clauses + post = Post.first + having_then_where = Post.having(id: post.id).where(title: post.title) + .having(id: post.id).where(title: post.title).group(:id) + + assert_equal [post], having_then_where + end + + def test_grouping_by_column_with_reserved_name + assert_equal [], Possession.select(:where).group(:where).to_a end def test_references_triggers_eager_loading @@ -1593,6 +1686,10 @@ class RelationTest < ActiveRecord::TestCase assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) } end + test "find_by requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1615,6 +1712,10 @@ class RelationTest < ActiveRecord::TestCase end end + test "find_by! requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by! } + end + test "loaded relations cannot be mutated by multi value methods" do relation = Post.all relation.to_a @@ -1744,14 +1845,13 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_keeps_lhs_bind_parameters - column = Post.columns_hash['id'] - binds = [[column, 20]] + binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))] right = Post.where(id: 20) left = Post.where(id: 10) merged = left.merge(right) - assert_equal binds, merged.bind_values + assert_equal binds, merged.bound_attributes end def test_merging_reorders_bind_params diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index b52c66356c..51170c533b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -3,7 +3,7 @@ require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do ActiveRecord::SchemaMigration.create_table @@ -73,10 +73,10 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+"/) match[0].length end - end + end.compact assert_equal 1, lengths.uniq.length end @@ -168,24 +168,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -204,50 +204,58 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_schema_dump_should_add_default_value_for_mysql_text_field output = standard_dump - assert_match %r{t.text\s+"body",\s+limit: 65535,\s+null: false$}, output + assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output end def test_schema_dump_includes_length_for_mysql_binary_fields output = standard_dump - assert_match %r{t.binary\s+"var_binary",\s+limit: 255$}, output - assert_match %r{t.binary\s+"var_binary_large",\s+limit: 4095$}, output + assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output + assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output end def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump - assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output - assert_match %r{t.binary\s+"normal_blob",\s+limit: 65535$}, output - assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output - assert_match %r{t.binary\s+"long_blob",\s+limit: 4294967295$}, output - assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output - assert_match %r{t.text\s+"normal_text",\s+limit: 65535$}, output - assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output - assert_match %r{t.text\s+"long_text",\s+limit: 4294967295$}, output + assert_match %r{t\.binary\s+"tiny_blob",\s+limit: 255$}, output + assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output + assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output + assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output + assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output + assert_match %r{t\.text\s+"normal_text",\s+limit: 65535$}, output + assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output + assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output end - def test_schema_dumps_index_type + def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields output = standard_dump - assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output end - end - if mysql_56? - def test_schema_dump_includes_datetime_precision + def test_schema_dumps_index_type output = standard_dump - assert_match %r{t.datetime\s+"written_on",\s+precision: 6$}, output + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end 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) def test_schema_dump_includes_bigint_default output = standard_dump - assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + end + + def test_schema_dump_includes_limit_on_array_type + output = standard_dump + 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? @@ -271,11 +279,11 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers if current_adapter?(:OracleAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output + assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 38}, output elsif current_adapter?(:FbAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 18}, output + assert_match %r{t\.integer\s+"atoms_in_universe",\s+precision: 18}, output else - assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output + assert_match %r{t\.decimal\s+"atoms_in_universe",\s+precision: 55}, output end end @@ -284,7 +292,7 @@ class SchemaDumperTest < ActiveRecord::TestCase match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n}) assert_not_nil(match, "goofy_string_id table not found") assert_match %r(id: false), match[1], "no table id not preserved" - assert_match %r{t.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved" + assert_match %r{t\.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved" end def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added @@ -362,7 +370,7 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase teardown do return unless @connection - @connection.execute 'DROP TABLE defaults' if @connection.table_exists? 'defaults' + @connection.drop_table 'defaults', if_exists: true end def test_schema_dump_defaults_with_universally_supported_types diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0738df1b54..0dbc60940e 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -153,6 +153,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected_7, received_7 end + def test_unscope_comparison_where_clauses + # unscoped for WHERE (`developers`.`id` <= 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + + # unscoped for WHERE (`developers`.`id` < 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + end + def test_unscope_multiple_where_clauses expected = Developer.order('salary DESC').collect(&:name) received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name) @@ -284,8 +296,8 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_merging merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where)) - assert merged.where_values.empty? - assert !merged.where(name: "Jon").where_values.empty? + assert merged.where_clause.empty? + assert !merged.where(name: "Jon").where_clause.empty? end def test_order_in_default_scope_should_not_prevail @@ -426,19 +438,19 @@ class DefaultScopingTest < ActiveRecord::TestCase test "additional conditions are ANDed with the default scope" do scope = DeveloperCalledJamis.where(name: "David") - assert_equal 2, scope.where_values.length + assert_equal 2, scope.where_clause.ast.children.length assert_equal [], scope.to_a end test "additional conditions in a scope are ANDed with the default scope" do scope = DeveloperCalledJamis.david - assert_equal 2, scope.where_values.length + assert_equal 2, scope.where_clause.ast.children.length assert_equal [], scope.to_a end test "a scope can remove the condition from the default scope" do scope = DeveloperCalledJamis.david2 - assert_equal 1, scope.where_values.length - assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id) + assert_equal 1, scope.where_clause.ast.children.length + assert_equal Developer.where(name: "David"), scope end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 41f3449828..e4cc533517 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -317,13 +317,15 @@ class NamedScopingTest < ActiveRecord::TestCase ] conflicts.each do |name| - assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do klass.class_eval { scope name, ->{ where(approved: true) } } end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) - assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do subklass.class_eval { scope name, ->{ where(approved: true) } } end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) end non_conflicts.each do |name| @@ -380,8 +382,8 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_should_not_duplicates_where_values - where_values = Topic.where("1=1").scope_with_lambda.where_values - assert_equal ["1=1"], where_values + relation = Topic.where("1=1") + assert_equal relation.where_clause, relation.scope_with_lambda.where_clause end def test_chaining_with_duplicate_joins diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index d7bcbf6203..4bfffbe9c6 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -184,7 +184,7 @@ class RelationScopingTest < ActiveRecord::TestCase rescue end - assert !Developer.all.where_values.include?("name = 'Jamis'") + assert_not Developer.all.to_sql.include?("name = 'Jamis'"), "scope was not restored" end def test_default_scope_filters_on_joins @@ -208,6 +208,12 @@ class RelationScopingTest < ActiveRecord::TestCase assert_equal [], DeveloperFilteredOnJoins.all assert_not_equal [], Developer.all end + + def test_current_scope_does_not_pollute_other_subclasses + Post.none.scoping do + assert StiPost.all.any? + end + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb index dc12b528dc..e731443fc2 100644 --- a/activerecord/test/cases/secure_token_test.rb +++ b/activerecord/test/cases/secure_token_test.rb @@ -23,17 +23,10 @@ class SecureTokenTest < ActiveRecord::TestCase assert_not_equal @user.auth_token, old_auth_token end - def test_raise_after_ten_unsuccessful_attempts_to_generate_a_unique_token - User.stubs(:exists?).returns(*Array.new(10, true)) - assert_raises(RuntimeError) do - @user.save - end - end - - def test_return_unique_token_after_nine_unsuccessful_attempts - User.stubs(:exists?).returns(*Array.new(10) { |i| i == 9 ? false : true }) + def test_token_value_not_overwritten_when_present + @user.token = "custom-secure-token" @user.save - assert_not_nil @user.token - assert_not_nil @user.auth_token + + assert_equal @user.token, "custom-secure-token" end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index e29f7462c8..a31c3c828c 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -264,4 +264,27 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize(:content, Regexp) end end + + def test_newly_emptied_serialized_hash_is_changed + Topic.serialize(:content, Hash) + topic = Topic.create(content: { "things" => "stuff" }) + topic.content.delete("things") + topic.save! + topic.reload + + 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 end diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb new file mode 100644 index 0000000000..72c5c16555 --- /dev/null +++ b/activerecord/test/cases/suppressor_test.rb @@ -0,0 +1,52 @@ +require 'cases/helper' +require 'models/notification' +require 'models/user' + +class SuppressorTest < ActiveRecord::TestCase + 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! } + end + end + end + + def test_resumes_saving_after_suppression_complete + Notification.suppress { UserWithNotification.create! } + + assert_difference -> { Notification.count } do + Notification.create! + end + end +end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 2fa033ed45..38164b2228 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -377,4 +377,20 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql") end end + + class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase + def test_check_schema_file_defaults + ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp') + assert_equal '/tmp/schema.rb', ActiveRecord::Tasks::DatabaseTasks.schema_file + end + end + + class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase + {ruby: 'schema.rb', sql: 'structure.sql'}.each_pair do |fmt, filename| + define_method("test_check_schema_file_for_#{fmt}_format") do + ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp') + assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) + end + end + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f58535f044..d0deb4c273 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -265,14 +265,14 @@ module ActiveRecord def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end def test_warn_when_external_structure_dump_fails filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(false) warnings = capture(:stderr) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) @@ -283,12 +283,21 @@ module ActiveRecord def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge('port' => 10000), filename) end + + def test_structure_dump_with_ssl + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("sslca" => "ca.crt"), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 0d574d071c..084302cde5 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -195,21 +195,54 @@ module ActiveRecord 'adapter' => 'postgresql', 'database' => 'my-app-db' } + @filename = "awesome-file.sql" ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) Kernel.stubs(:system) + File.stubs(:open) end def test_structure_dump - filename = "awesome-file.sql" - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true) - @connection.expects(:schema_search_path).returns("foo") + 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 + + 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) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + + 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) + + with_dump_schemas(:all) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + 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) + + with_dump_schemas('foo,bar') do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end + + private - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exist?(filename) + def with_dump_schemas(value, &block) + old_dump_schemas = ActiveRecord::Base.dump_schemas + ActiveRecord::Base.dump_schemas = value + yield ensure - FileUtils.rm(filename) + ActiveRecord::Base.dump_schemas = old_dump_schemas end end @@ -228,14 +261,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -q -f #{filename} my-app-db") + Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db") 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 -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db") 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 5ba17359f0..7761ea5612 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,10 +1,13 @@ require 'active_support/test_case' +require 'active_support/testing/stream' module ActiveRecord # = Active Record Test Case # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: + include ActiveSupport::Testing::Stream + def teardown SQLCounter.clear_log end @@ -13,23 +16,6 @@ module ActiveRecord assert_equal expected.to_s, actual.to_s, message end - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end - def capture_sql SQLCounter.clear_log yield @@ -79,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 @@ -95,7 +105,7 @@ module ActiveRecord oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] + sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| ignored_sql.concat db_ignored_sql diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb new file mode 100644 index 0000000000..3f4baf8378 --- /dev/null +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +class TestFixturesTest < ActiveRecord::TestCase + setup do + @klass = Class.new + @klass.send(:include, ActiveRecord::TestFixtures) + end + + def test_deprecated_use_transactional_fixtures= + assert_deprecated 'use use_transactional_tests= instead' do + @klass.use_transactional_fixtures = true + end + end + + def test_use_transactional_tests_prefers_use_transactional_fixtures + ActiveSupport::Deprecation.silence do + @klass.use_transactional_fixtures = false + end + + assert_equal false, @klass.use_transactional_tests + end + + def test_use_transactional_tests_defaults_to_true + ActiveSupport::Deprecation.silence do + @klass.use_transactional_fixtures = nil + end + + assert_equal true, @klass.use_transactional_tests + end + + def test_use_transactional_tests_can_be_overriden + @klass.use_transactional_tests = "foobar" + + assert_equal "foobar", @klass.use_transactional_tests + end +end diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb new file mode 100644 index 0000000000..ff7a81fe60 --- /dev/null +++ b/activerecord/test/cases/time_precision_test.rb @@ -0,0 +1,108 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_datetime_with_precision? +class TimePrecisionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + class Foo < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + end + + teardown do + @connection.drop_table :foos, if_exists: true + end + + def test_time_data_type_with_precision + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 3 + @connection.add_column :foos, :finish, :time, precision: 6 + assert_equal 3, activerecord_column_option('foos', 'start', 'precision') + assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + end + + def test_passing_precision_to_time_does_not_set_limit + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 3 + t.time :finish, precision: 6 + end + assert_nil activerecord_column_option('foos', 'start', 'limit') + assert_nil activerecord_column_option('foos', 'finish', 'limit') + end + + def test_invalid_time_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 7 + t.time :finish, precision: 7 + end + end + end + + def test_database_agrees_with_activerecord_about_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 2 + t.time :finish, precision: 4 + end + assert_equal 2, database_datetime_precision('foos', 'start') + assert_equal 4, database_datetime_precision('foos', 'finish') + end + + def test_formatting_time_according_to_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 0 + t.time :finish, precision: 4 + end + time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) + Foo.create!(start: time, finish: time) + assert foo = Foo.find_by(start: time) + assert_equal 1, Foo.where(finish: time).count + assert_equal time.to_s, foo.start.to_s + assert_equal time.to_s, foo.finish.to_s + assert_equal 000000, foo.start.usec + assert_equal 999900, foo.finish.usec + end + + def test_schema_dump_includes_time_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 4 + t.time :finish, precision: 6 + end + output = dump_table_schema("foos") + assert_match %r{t\.time\s+"start",\s+precision: 4$}, output + assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output + end + + if current_adapter?(:PostgreSQLAdapter) + def test_time_precision_with_zero_should_be_dumped + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 0 + t.time :finish, precision: 0 + end + output = dump_table_schema("foos") + assert_match %r{t\.time\s+"start",\s+precision: 0$}, output + assert_match %r{t\.time\s+"finish",\s+precision: 0$}, output + end + end + + private + + def database_datetime_precision(table_name, column_name) + results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"].to_i + end + + def activerecord_column_option(tablename, column_name, option) + result = @connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end +end +end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index db474c63a4..5dab32995c 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -73,6 +73,15 @@ class TimestampTest < ActiveRecord::TestCase assert_equal @previously_updated_at, @developer.updated_at end + def test_touching_updates_timestamp_with_given_time + previously_updated_at = @developer.updated_at + new_time = Time.utc(2015, 2, 16, 0, 0, 0) + @developer.touch(time: new_time) + + assert_not_equal previously_updated_at, @developer.updated_at + assert_equal new_time, @developer.updated_at + end + def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at @developer.touch(:created_at) @@ -91,6 +100,18 @@ class TimestampTest < ActiveRecord::TestCase assert_in_delta Time.now, task.ending, 1 end + def test_touching_an_attribute_updates_timestamp_with_given_time + previously_updated_at = @developer.updated_at + previously_created_at = @developer.created_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + @developer.touch(:created_at, time: new_time) + + assert_not_equal previously_created_at, @developer.created_at + assert_not_equal previously_updated_at, @developer.updated_at + assert_equal new_time, @developer.created_at + assert_equal new_time, @developer.updated_at + end + def test_touching_many_attributes_updates_them task = Task.first previous_starting = task.starting @@ -425,11 +446,22 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps(:foos, null: true, index: true) + end + + indexes = ActiveRecord::Base.connection.indexes('foos') + assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase include DdlHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class TimestampAttributePost < ActiveRecord::Base attr_accessor :created_at, :updated_at diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..11804ff90b --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' + +class TouchLaterTest < ActiveRecord::TestCase + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + refute invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + refute invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 185fc22e98..f2229939c8 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -4,7 +4,6 @@ require 'models/pet' require 'models/topic' class TransactionCallbacksTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false fixtures :topics, :owners, :pets class ReplyWithCallbacks < ActiveRecord::Base @@ -200,21 +199,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_rollback_when_commit_fails - @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) - begin - @first.class.connection.singleton_class.class_eval do - def commit_db_transaction; raise "boom!"; end - end + @first.after_commit_block { |r| r.history << :after_commit } + @first.after_rollback_block { |r| r.history << :after_rollback } - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} + assert_raises RuntimeError do + @first.transaction do + tx = @first.class.connection.transaction_manager.current_transaction + def tx.commit + raise + end - assert !@first.save rescue nil - assert_equal [:after_rollback], @first.history - ensure - @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction) - @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + @first.save + end end + + assert_equal [:after_rollback], @first.history end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint @@ -368,7 +367,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base self.table_name = :topics @@ -377,6 +376,9 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase after_commit(on: [:create, :update]) { |record| record.history << :create_and_update } after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy } + before_commit(if: :save_before_commit_history) { |record| record.history << :before_commit } + before_commit(if: :update_title) { |record| record.update(title: "before commit title") } + def clear_history @history = [] end @@ -384,6 +386,8 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase def history @history ||= [] end + + attr_accessor :save_before_commit_history, :update_title end def test_after_commit_on_multiple_actions @@ -400,4 +404,81 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase topic.destroy assert_equal [:update_and_destroy, :create_and_destroy], topic.history end + + def test_before_commit_actions + topic = TopicWithCallbacksOnMultipleActions.new + topic.save_before_commit_history = true + topic.save + + assert_equal [:before_commit, :create_and_update, :create_and_destroy], topic.history + end + + def test_before_commit_update_in_same_transaction + topic = TopicWithCallbacksOnMultipleActions.new + topic.update_title = true + topic.save + + assert_equal "before commit title", topic.title + assert_equal "before commit title", topic.reload.title + end +end + + +class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase + + class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base + self.table_name = :topics + + before_commit_without_transaction_enrollment { |r| r.history << :before_commit } + after_commit_without_transaction_enrollment { |r| r.history << :after_commit } + after_rollback_without_transaction_enrollment { |r| r.history << :rollback } + + def history + @history ||= [] + end + end + + def setup + @topic = TopicWithoutTransactionalEnrollmentCallbacks.create! + end + + def test_commit_does_not_run_transactions_callbacks_without_enrollment + @topic.transaction do + @topic.content = 'foo' + @topic.save! + end + assert @topic.history.empty? + end + + def test_commit_run_transactions_callbacks_with_explicit_enrollment + @topic.transaction do + 2.times do + @topic.content = 'foo' + @topic.save! + end + @topic.class.connection.add_transaction_record(@topic) + end + assert_equal [:before_commit, :after_commit], @topic.history + end + + def test_rollback_does_not_run_transactions_callbacks_without_enrollment + @topic.transaction do + @topic.content = 'foo' + @topic.save! + raise ActiveRecord::Rollback + end + assert @topic.history.empty? + end + + def test_rollback_run_transactions_callbacks_with_explicit_enrollment + @topic.transaction do + 2.times do + @topic.content = 'foo' + @topic.save! + end + @topic.class.connection.add_transaction_record(@topic) + raise ActiveRecord::Rollback + end + assert_equal [:rollback], @topic.history + end end diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index f89c26532d..2f7d208ed2 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' unless ActiveRecord::Base.connection.supports_transaction_isolation? class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Tag < ActiveRecord::Base end @@ -17,7 +17,7 @@ end if ActiveRecord::Base.connection.supports_transaction_isolation? class TransactionIsolationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Tag < ActiveRecord::Base self.table_name = 'tags' diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index d1d8e71c34..2468a91969 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -9,7 +9,7 @@ require 'models/post' require 'models/movie' class TransactionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :topics, :developers, :authors, :posts def setup @@ -584,6 +584,24 @@ class TransactionTest < ActiveRecord::TestCase assert_not topic.frozen? end + def test_rollback_of_frozen_records + topic = Topic.create.freeze + Topic.transaction do + topic.destroy + raise ActiveRecord::Rollback + end + assert topic.frozen?, 'frozen' + end + + def test_rollback_for_freshly_persisted_records + topic = Topic.create + Topic.transaction do + topic.destroy + raise ActiveRecord::Rollback + end + assert topic.persisted?, 'persisted' + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) @@ -668,7 +686,7 @@ class TransactionTest < ActiveRecord::TestCase end end ensure - connection.execute("DROP TABLE IF EXISTS transaction_without_primary_keys") + connection.drop_table 'transaction_without_primary_keys', if_exists: true end private @@ -685,7 +703,7 @@ class TransactionTest < ActiveRecord::TestCase end class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = true + self.use_transactional_tests = true fixtures :topics def test_automatic_savepoint_in_outer_transaction diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb new file mode 100644 index 0000000000..8b836b4793 --- /dev/null +++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb @@ -0,0 +1,133 @@ +require "cases/helper" + +module ActiveRecord + class AdapterSpecificRegistryTest < ActiveRecord::TestCase + test "a class can be registered for a symbol" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) + + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) + end + + test "a block can be registered" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + end + registry.register(:bar) do |*args| + [*args, "block for bar"] + end + + assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) + assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) + assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) + end + + test "filtering by adapter" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, adapter: :sqlite3) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + assert_equal [], registry.lookup(:foo, adapter: :postgresql) + end + + test "an error is raised if both a generic and adapter specific type match" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.register(:foo, Array, adapter: :postgresql) + + assert_raises TypeConflictError do + registry.lookup(:foo, adapter: :postgresql) + end + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a generic type can explicitly override an adapter specific type" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, override: true) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal "", registry.lookup(:foo, adapter: :postgresql) + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a generic type can explicitly allow an adapter type to be used instead" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, override: false) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal [], registry.lookup(:foo, adapter: :postgresql) + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a reasonable error is given when no type is found" do + registry = Type::AdapterSpecificRegistry.new + + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end + + assert_equal "Unknown type :foo", e.message + end + + test "construct args are passed to the type" do + type = Struct.new(:args) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, type) + + assert_equal type.new, registry.lookup(:foo) + assert_equal type.new(:ordered_arg), registry.lookup(:foo, :ordered_arg) + assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg) + assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg, adapter: :postgresql) + end + + test "registering a modifier" do + decoration = Struct.new(:value) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.register(:bar, Hash) + registry.add_modifier({ array: true }, decoration) + + assert_equal decoration.new(""), registry.lookup(:foo, array: true) + assert_equal decoration.new({}), registry.lookup(:bar, array: true) + assert_equal "", registry.lookup(:foo) + end + + test "registering multiple modifiers" do + decoration = Struct.new(:value) + other_decoration = Struct.new(:value) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.add_modifier({ array: true }, decoration) + registry.add_modifier({ range: true }, other_decoration) + + assert_equal "", registry.lookup(:foo) + assert_equal decoration.new(""), registry.lookup(:foo, array: true) + assert_equal other_decoration.new(""), registry.lookup(:foo, range: true) + assert_equal( + decoration.new(other_decoration.new("")), + registry.lookup(:foo, array: true, range: true) + ) + end + + test "registering adapter specific modifiers" do + decoration = Struct.new(:value) + type = Struct.new(:args) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, type) + registry.add_modifier({ array: true }, decoration, adapter: :postgresql) + + assert_equal( + decoration.new(type.new(keyword: :arg)), + registry.lookup(:foo, array: true, adapter: :postgresql, keyword: :arg) + ) + assert_equal( + type.new(array: true), + registry.lookup(:foo, array: true, adapter: :sqlite3) + ) + end + end +end diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb index 34ed1d7b19..fe49d0e79a 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -5,29 +5,29 @@ module ActiveRecord class DecimalTest < ActiveRecord::TestCase def test_type_cast_decimal type = Decimal.new - assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) - assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.cast(123.0) + assert_equal BigDecimal.new("1"), type.cast(:"1") end def test_type_cast_decimal_from_float_with_large_precision type = Decimal.new(precision: ::Float::DIG + 2) - assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("123.0"), type.cast(123.0) end def test_type_cast_from_float_with_unspecified_precision type = Decimal.new - assert_equal 22.68.to_d, type.type_cast_from_user(22.68) + assert_equal 22.68.to_d, type.cast(22.68) end def test_type_cast_decimal_from_rational_with_precision type = Decimal.new(precision: 2) - assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) end def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 type = Decimal.new - assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3)) end def test_type_cast_decimal_from_object_responding_to_d @@ -36,7 +36,7 @@ module ActiveRecord BigDecimal.new("1") end type = Decimal.new - assert_equal BigDecimal("1"), type.type_cast_from_user(value) + assert_equal BigDecimal("1"), type.cast(value) end def test_changed? diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index ff956b7680..0dcdbd0667 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -6,45 +6,45 @@ module ActiveRecord class IntegerTest < ActiveRecord::TestCase test "simple values" do type = Type::Integer.new - assert_equal 1, type.type_cast_from_user(1) - assert_equal 1, type.type_cast_from_user('1') - assert_equal 1, type.type_cast_from_user('1ignore') - assert_equal 0, type.type_cast_from_user('bad1') - assert_equal 0, type.type_cast_from_user('bad') - assert_equal 1, type.type_cast_from_user(1.7) - assert_equal 0, type.type_cast_from_user(false) - assert_equal 1, type.type_cast_from_user(true) - assert_nil type.type_cast_from_user(nil) + assert_equal 1, type.cast(1) + assert_equal 1, type.cast('1') + assert_equal 1, type.cast('1ignore') + assert_equal 0, type.cast('bad1') + assert_equal 0, type.cast('bad') + assert_equal 1, type.cast(1.7) + assert_equal 0, type.cast(false) + assert_equal 1, type.cast(true) + assert_nil type.cast(nil) end test "random objects cast to nil" do type = Type::Integer.new - assert_nil type.type_cast_from_user([1,2]) - assert_nil type.type_cast_from_user({1 => 2}) - assert_nil type.type_cast_from_user((1..2)) + assert_nil type.cast([1,2]) + assert_nil type.cast({1 => 2}) + assert_nil type.cast(1..2) end test "casting ActiveRecord models" do type = Type::Integer.new firm = Firm.create(:name => 'Apple') - assert_nil type.type_cast_from_user(firm) + assert_nil type.cast(firm) end test "casting objects without to_i" do type = Type::Integer.new - assert_nil type.type_cast_from_user(::Object.new) + assert_nil type.cast(::Object.new) end test "casting nan and infinity" do type = Type::Integer.new - assert_nil type.type_cast_from_user(::Float::NAN) - assert_nil type.type_cast_from_user(1.0/0.0) + assert_nil type.cast(::Float::NAN) + assert_nil type.cast(1.0/0.0) end test "casting booleans for database" do type = Type::Integer.new - assert_equal 1, type.type_cast_for_database(true) - assert_equal 0, type.type_cast_for_database(false) + assert_equal 1, type.serialize(true) + assert_equal 0, type.serialize(false) end test "changed?" do @@ -60,55 +60,68 @@ module ActiveRecord test "values below int min value are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_from_user("-2147483649") + Integer.new.serialize(-2147483649) end end test "values above int max value are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_from_user("2147483648") + Integer.new.serialize(2147483648) end end test "very small numbers are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_from_user("-9999999999999999999999999999999") + Integer.new.serialize(-9999999999999999999999999999999) end end test "very large numbers are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_from_user("9999999999999999999999999999999") + Integer.new.serialize(9999999999999999999999999999999) end end test "normal numbers are in range" do type = Integer.new - assert_equal(0, type.type_cast_from_user("0")) - assert_equal(-1, type.type_cast_from_user("-1")) - assert_equal(1, type.type_cast_from_user("1")) + assert_equal(0, type.serialize(0)) + assert_equal(-1, type.serialize(-1)) + assert_equal(1, type.serialize(1)) end test "int max value is in range" do - assert_equal(2147483647, Integer.new.type_cast_from_user("2147483647")) + assert_equal(2147483647, Integer.new.serialize(2147483647)) end test "int min value is in range" do - assert_equal(-2147483648, Integer.new.type_cast_from_user("-2147483648")) + assert_equal(-2147483648, Integer.new.serialize(-2147483648)) end test "columns with a larger limit have larger ranges" do type = Integer.new(limit: 8) - assert_equal(9223372036854775807, type.type_cast_from_user("9223372036854775807")) - assert_equal(-9223372036854775808, type.type_cast_from_user("-9223372036854775808")) + assert_equal(9223372036854775807, type.serialize(9223372036854775807)) + assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) assert_raises(::RangeError) do - type.type_cast_from_user("-9999999999999999999999999999999") + type.serialize(-9999999999999999999999999999999) end assert_raises(::RangeError) do - type.type_cast_from_user("9999999999999999999999999999999") + type.serialize(9999999999999999999999999999999) end end + + test "values which are out of range can be re-assigned" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'posts' + attribute :foo, :integer + end + model = klass.new + + model.foo = 2147483648 + model.foo = 1 + + assert_equal 1, model.foo + end end end end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 4d78f287f1..56e9bf434d 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -4,16 +4,16 @@ module ActiveRecord class StringTypeTest < ActiveRecord::TestCase test "type casting" do type = Type::String.new - assert_equal "t", type.type_cast_from_user(true) - assert_equal "f", type.type_cast_from_user(false) - assert_equal "123", type.type_cast_from_user(123) + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) end test "values are duped coming out" do s = "foo" type = Type::String.new - assert_not_same s, type.type_cast_from_user(s) - assert_not_same s, type.type_cast_from_database(s) + assert_not_same s, type.cast(s) + assert_not_same s, type.deserialize(s) end test "string mutations are detected" do diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb index b6f673109e..f2c910eade 100644 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -4,12 +4,12 @@ module ActiveRecord module Type class UnsignedIntegerTest < ActiveRecord::TestCase test "unsigned int max value is in range" do - assert_equal(4294967295, UnsignedInteger.new.type_cast_from_user("4294967295")) + assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) end test "minus value is out of range" do assert_raises(::RangeError) do - UnsignedInteger.new.type_cast_from_user("-1") + UnsignedInteger.new.serialize(-1) end end end diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb new file mode 100644 index 0000000000..d45a9b3141 --- /dev/null +++ b/activerecord/test/cases/type_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" + +class TypeTest < ActiveRecord::TestCase + setup do + @old_registry = ActiveRecord::Type.registry + ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new + end + + teardown do + ActiveRecord::Type.registry = @old_registry + end + + test "registering a new type" do + type = Struct.new(:args) + ActiveRecord::Type.register(:foo, type) + + assert_equal type.new(:arg), ActiveRecord::Type.lookup(:foo, :arg) + end + + test "looking up a type for a specific adapter" do + type = Struct.new(:args) + pgtype = Struct.new(:args) + ActiveRecord::Type.register(:foo, type, override: false) + ActiveRecord::Type.register(:foo, pgtype, adapter: :postgresql) + + assert_equal type.new, ActiveRecord::Type.lookup(:foo, adapter: :sqlite) + assert_equal pgtype.new, ActiveRecord::Type.lookup(:foo, adapter: :postgresql) + end + + test "lookup defaults to the current adapter" do + current_adapter = ActiveRecord::Base.connection.adapter_name.downcase.to_sym + type = Struct.new(:args) + adapter_type = Struct.new(:args) + ActiveRecord::Type.register(:foo, type, override: false) + ActiveRecord::Type.register(:foo, adapter_type, adapter: current_adapter) + + assert_equal adapter_type.new, ActiveRecord::Type.lookup(:foo) + end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 73e92addfe..9b1859c2ce 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -5,38 +5,38 @@ module ActiveRecord class TypesTest < ActiveRecord::TestCase def test_type_cast_boolean type = Type::Boolean.new - assert type.type_cast_from_user('').nil? - assert type.type_cast_from_user(nil).nil? - - assert type.type_cast_from_user(true) - assert type.type_cast_from_user(1) - assert type.type_cast_from_user('1') - assert type.type_cast_from_user('t') - assert type.type_cast_from_user('T') - assert type.type_cast_from_user('true') - assert type.type_cast_from_user('TRUE') - assert type.type_cast_from_user('on') - assert type.type_cast_from_user('ON') - assert type.type_cast_from_user(' ') - assert type.type_cast_from_user("\u3000\r\n") - assert type.type_cast_from_user("\u0000") - assert type.type_cast_from_user('SOMETHING RANDOM') + assert type.cast('').nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast('1') + assert type.cast('t') + assert type.cast('T') + assert type.cast('true') + assert type.cast('TRUE') + assert type.cast('on') + assert type.cast('ON') + assert type.cast(' ') + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast('SOMETHING RANDOM') # explicitly check for false vs nil - assert_equal false, type.type_cast_from_user(false) - assert_equal false, type.type_cast_from_user(0) - assert_equal false, type.type_cast_from_user('0') - assert_equal false, type.type_cast_from_user('f') - assert_equal false, type.type_cast_from_user('F') - assert_equal false, type.type_cast_from_user('false') - assert_equal false, type.type_cast_from_user('FALSE') - assert_equal false, type.type_cast_from_user('off') - assert_equal false, type.type_cast_from_user('OFF') + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') end def test_type_cast_float type = Type::Float.new - assert_equal 1.0, type.type_cast_from_user("1") + assert_equal 1.0, type.cast("1") end def test_changing_float @@ -50,54 +50,54 @@ module ActiveRecord def test_type_cast_binary type = Type::Binary.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal "1", type.type_cast_from_user("1") - assert_equal 1, type.type_cast_from_user(1) + assert_equal nil, type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) end def test_type_cast_time type = Type::Time.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast('ABC') time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") end def test_type_cast_datetime_and_timestamp type = Type::DateTime.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user(' ') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") end def test_type_cast_date type = Type::Date.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user(' ') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") end def test_type_cast_duration_to_integer type = Type::Integer.new - assert_equal 1800, type.type_cast_from_user(30.minutes) - assert_equal 7200, type.type_cast_from_user(2.hours) + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) end def test_string_to_time_with_timezone [:utc, :local].each do |zone| with_timezone_config default: zone do type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT") + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") end end end @@ -108,14 +108,21 @@ module ActiveRecord assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) end - if current_adapter?(:SQLite3Adapter) - def test_binary_encoding - type = SQLite3Binary.new - utf8_string = "a string".encode(Encoding::UTF_8) - type_cast = type.type_cast_from_user(utf8_string) - - assert_equal Encoding::ASCII_8BIT, type_cast.encoding + def test_attributes_which_are_invalid_for_database_can_still_be_reassigned + type_which_cannot_go_to_the_database = Type::Value.new + def type_which_cannot_go_to_the_database.serialize(*) + raise + end + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'posts' + attribute :foo, type_which_cannot_go_to_the_database end + model = klass.new + + model.foo = "foo" + model.foo = "bar" + + assert_equal "bar", model.foo end end end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index afb893a52c..b210584644 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestUnconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @underlying = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index e4edc437e6..bff5ffa65e 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -50,7 +50,7 @@ class AssociationValidationTest < ActiveRecord::TestCase Topic.validates_presence_of :content r = Reply.create("title" => "A reply", "content" => "with content!") r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? + assert_not_operator r, :valid? assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic] end @@ -82,5 +82,4 @@ class AssociationValidationTest < ActiveRecord::TestCase assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated" end end - end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 2c0e282761..f95f8f0b8f 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -6,49 +6,49 @@ require 'models/person' class LengthValidationTest < ActiveRecord::TestCase fixtures :owners - repair_validations(Owner) - def test_validates_size_of_association - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'apet') - assert o.valid? + setup do + @owner = Class.new(Owner) do + def self.name; 'Owner'; end end end + + def test_validates_size_of_association + assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 } + o = @owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'apet') + assert o.valid? + end + def test_validates_size_of_association_using_within - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? + assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 } + o = @owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? - o.pets.build('name' => 'apet') - assert o.valid? + o.pets.build('name' => 'apet') + assert o.valid? - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors[:pets].any? - end + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? end def test_validates_size_of_association_utf8 - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? - end + @owner.validates_size_of :pets, minimum: 1 + o = @owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? end - def test_validates_size_of_reprects_records_marked_for_destruction - assert_nothing_raised { Owner.validates_size_of :pets, minimum: 1 } - owner = Owner.new + def test_validates_size_of_respects_records_marked_for_destruction + @owner.validates_size_of :pets, minimum: 1 + owner = @owner.new assert_not owner.save assert owner.errors[:pets].any? pet = owner.pets.build @@ -62,4 +62,17 @@ class LengthValidationTest < ActiveRecord::TestCase assert_equal pet_count, Pet.count end + def test_does_not_validate_length_of_if_parent_record_is_validate_false + @owner.validates_length_of :name, minimum: 1 + owner = @owner.new + owner.save!(validate: false) + assert owner.persisted? + + pet = Pet.new(owner_id: owner.id) + pet.save! + + assert_equal owner.pets.size, 1 + assert owner.valid? + assert pet.valid? + end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 4f38849131..6f8ad06ab6 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'models/man' require 'models/face' @@ -65,4 +64,20 @@ class PresenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { s.valid? } end + + def test_does_not_validate_presence_of_if_parent_record_is_validate_false + repair_validations(Interest) do + Interest.validates_presence_of(:topic) + interest = Interest.new + 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 524f59876e..2608c84be2 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'models/topic' require 'models/reply' @@ -35,7 +34,22 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -87,6 +101,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) @@ -386,4 +410,21 @@ class UniquenessValidationTest < ActiveRecord::TestCase topic = TopicWithUniqEvent.new assert topic.valid? end + + def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false + Reply.validates_uniqueness_of(:content) + + Reply.create!(content: "Topic Title") + + reply = Reply.new(content: "Topic Title") + reply.save!(validate: false) + assert reply.persisted? + + topic = Topic.new(reply_ids: [reply.id]) + topic.save! + + assert_equal topic.replies.size, 1 + assert reply.valid? + assert topic.valid? + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 959c58aa85..f4f316f393 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'models/topic' require 'models/reply' @@ -151,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_numericality_validation_with_mutation Topic.class_eval do - attribute :wibble, ActiveRecord::Type::String.new + attribute :wibble, :string validates_numericality_of :wibble, only_integer: true end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 3aed90ba36..f9dca1e196 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -102,7 +102,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end def test_attributes - assert_equal({"name" => "Agile Web Development with Rails", "status" => 0}, + assert_equal({"name" => "Agile Web Development with Rails", "status" => 2}, Paperback.first.attributes) end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index c34e7d5a30..b30b50f597 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -19,7 +19,7 @@ class XmlSerializationTest < ActiveRecord::TestCase 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 xmlns="http://xml\.rubyonrails\.org/contact">}, @xml assert_match %r{</contact>$}, @xml end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index bce59b4fcd..56909a8630 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -83,4 +83,39 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal 5, author.posts_count assert_equal 5, dumped.posts_count end + + def test_a_yaml_version_is_provided_for_future_backwards_compat + coder = {} + Topic.first.encode_with(coder) + + assert coder['active_record_yaml_version'] + end + + def test_deserializing_rails_41_yaml + topic = YAML.load(yaml_fixture("rails_4_1")) + + assert topic.new_record? + assert_equal nil, topic.id + assert_equal "The First Topic", topic.title + assert_equal({ omg: :lol }, topic.content) + end + + def test_deserializing_rails_4_2_0_yaml + topic = YAML.load(yaml_fixture("rails_4_2_0")) + + assert_not topic.new_record? + assert_equal 1, topic.id + assert_equal "The First Topic", topic.title + assert_equal("Have a nice day", topic.content) + end + + private + + def yaml_fixture(file_name) + path = File.expand_path( + "../../support/yaml_compatibility_fixtures/#{file_name}.yml", + __FILE__ + ) + File.read(path) + end end diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index abe56752c6..93cfabd61c 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -3,9 +3,24 @@ awdr: id: 1 name: "Agile Web Development with Rails" format: "paperback" + status: :published + read_status: :read + language: :english + author_visibility: :visible + illustrator_visibility: :visible + font_size: :medium rfr: author_id: 1 id: 2 name: "Ruby for Rails" format: "ebook" + status: "proposed" + read_status: "reading" + +ddd: + author_id: 1 + id: 3 + name: "Domain-Driven Design" + format: "hardcover" + status: 2 diff --git a/activerecord/test/fixtures/dead_parrots.yml b/activerecord/test/fixtures/dead_parrots.yml new file mode 100644 index 0000000000..da5529dd27 --- /dev/null +++ b/activerecord/test/fixtures/dead_parrots.yml @@ -0,0 +1,5 @@ +deadbird: + name: "Dusty DeadBird" + treasures: [ruby, sapphire] + parrot_sti_class: DeadParrot + killer: blackbeard 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/live_parrots.yml b/activerecord/test/fixtures/live_parrots.yml new file mode 100644 index 0000000000..95b2078da7 --- /dev/null +++ b/activerecord/test/fixtures/live_parrots.yml @@ -0,0 +1,4 @@ +dusty: + name: "Dusty Bluebird" + treasures: [ruby, sapphire] + parrot_sti_class: LiveParrot diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml index 1bb3bf0051..0b1a785853 100644 --- a/activerecord/test/fixtures/pirates.yml +++ b/activerecord/test/fixtures/pirates.yml @@ -10,3 +10,6 @@ redbeard: mark: catchphrase: "X $LABELs the spot!" + +1: + catchphrase: "#$LABEL pirate!" diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb index 9fd495b97c..4b83d61beb 100644 --- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb +++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb @@ -6,4 +6,4 @@ class PeopleHaveMiddleNames < ActiveRecord::Migration def self.down remove_column "people", "middle_name" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb index 81af5fef5e..68209f3ce9 100644 --- a/activerecord/test/migrations/missing/1_people_have_last_names.rb +++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb @@ -6,4 +6,4 @@ class PeopleHaveLastNames < ActiveRecord::Migration def self.down remove_column "people", "last_name" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/missing/3_we_need_reminders.rb +++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/missing/4_innocent_jointable.rb +++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb index cdbe0b1679..f5484ac54f 100644 --- a/activerecord/test/migrations/rename/1_we_need_things.rb +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -8,4 +8,4 @@ class WeNeedThings < ActiveRecord::Migration def self.down drop_table "things" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb index d441b71fc9..533a113ea8 100644 --- a/activerecord/test/migrations/rename/2_rename_things.rb +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -6,4 +6,4 @@ class RenameThings < ActiveRecord::Migration def self.down rename_table "awesome_things", "things" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/valid/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/valid/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb index 00e69fbed8..a38e3f4846 100644 --- a/activerecord/test/models/admin.rb +++ b/activerecord/test/models/admin.rb @@ -2,4 +2,4 @@ module Admin def self.table_name_prefix 'admin_' end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb index 46de28aae1..bd23192d20 100644 --- a/activerecord/test/models/admin/account.rb +++ b/activerecord/test/models/admin/account.rb @@ -1,3 +1,3 @@ class Admin::Account < ActiveRecord::Base has_many :users -end
\ No newline at end of file +end diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb index 2f81d5b831..b64ae7fc41 100644 --- a/activerecord/test/models/admin/randomly_named_c1.rb +++ b/activerecord/test/models/admin/randomly_named_c1.rb @@ -1,3 +1,7 @@ -class Admin::ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
+ self.table_name = :randomly_named_table2
+end
+
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
+ self.table_name = :randomly_named_table3
end
diff --git a/activerecord/test/models/binary.rb b/activerecord/test/models/binary.rb index 950c459199..39b2f5090a 100644 --- a/activerecord/test/models/binary.rb +++ b/activerecord/test/models/binary.rb @@ -1,2 +1,2 @@ class Binary < ActiveRecord::Base -end
\ No newline at end of file +end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 2170018068..24bfe47bbf 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], enum_prefix: :in + enum author_visibility: [:visible, :invisible], enum_prefix: true + enum illustrator_visibility: [:visible, :invisible], enum_prefix: true + enum font_size: [:small, :medium, :large], enum_prefix: :with, enum_suffix: true def published! super diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index db0f93f63b..81263b79d1 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -8,7 +8,7 @@ class Car < ActiveRecord::Base has_one :bulb has_many :tyres - has_many :engines, :dependent => :destroy + has_many :engines, :dependent => :destroy, inverse_of: :my_car has_many :wheels, :as => :wheelable, :dependent => :destroy scope :incl_tyres, -> { includes(:tyres) } diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb index 67a4e54f06..698a52e045 100644 --- a/activerecord/test/models/chef.rb +++ b/activerecord/test/models/chef.rb @@ -1,3 +1,4 @@ class Chef < ActiveRecord::Base belongs_to :employable, polymorphic: true + has_many :recipes end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 5a56616eb9..67936e8e5d 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -26,6 +26,9 @@ class Company < AbstractCompany def private_method "I am Jack's innermost fears and aspirations" end + + class SpecialCo < Company + end end module Namespaced @@ -214,7 +217,7 @@ class Account < ActiveRecord::Base protected def check_empty_credit_limit - errors.add_on_empty "credit_limit" + errors.add("credit_limit", :blank) if credit_limit.blank? end private diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index dae102d12b..bf0a0d1c3e 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -91,7 +91,7 @@ module MyApplication protected def check_empty_credit_limit - errors.add_on_empty "credit_limit" + errors.add("credit_card", :blank) if credit_card.blank? end 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/event.rb b/activerecord/test/models/event.rb index 99fa0feeb7..365ab32b0b 100644 --- a/activerecord/test/models/event.rb +++ b/activerecord/test/models/event.rb @@ -1,3 +1,3 @@ class Event < ActiveRecord::Base validates_uniqueness_of :title -end
\ No newline at end of file +end diff --git a/activerecord/test/models/guid.rb b/activerecord/test/models/guid.rb index 9208dc28fa..05653ba498 100644 --- a/activerecord/test/models/guid.rb +++ b/activerecord/test/models/guid.rb @@ -1,2 +1,2 @@ class Guid < ActiveRecord::Base -end
\ No newline at end of file +end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb index b352cd22f3..491f8dfde3 100644 --- a/activerecord/test/models/hotel.rb +++ b/activerecord/test/models/hotel.rb @@ -3,4 +3,5 @@ class Hotel < ActiveRecord::Base has_many :chefs, through: :departments has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs + has_many :recipes, through: :chefs end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb new file mode 100644 index 0000000000..b4b4b8f1b6 --- /dev/null +++ b/activerecord/test/models/notification.rb @@ -0,0 +1,2 @@ +class Notification < ActiveRecord::Base +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 366c70f902..30545bdcd7 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -89,4 +89,4 @@ class FamousPirate < ActiveRecord::Base self.table_name = 'pirates' has_many :famous_ships validates_presence_of :catchphrase, on: :conference -end
\ No newline at end of file +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 7b637c9e3f..052b1c9690 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -18,6 +18,7 @@ class Post < ActiveRecord::Base end scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } + scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") } scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda {|l| limit(l) } @@ -43,6 +44,8 @@ class Post < ActiveRecord::Base scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) } scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) } + scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) } + has_many :comments do def find_most_recent order("id DESC").first diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb index 18a86c4989..d4be1e13b4 100644 --- a/activerecord/test/models/randomly_named_c1.rb +++ b/activerecord/test/models/randomly_named_c1.rb @@ -1,3 +1,3 @@ class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table
+ self.table_name = :randomly_named_table1
end
diff --git a/activerecord/test/models/recipe.rb b/activerecord/test/models/recipe.rb new file mode 100644 index 0000000000..c387230603 --- /dev/null +++ b/activerecord/test/models/recipe.rb @@ -0,0 +1,3 @@ +class Recipe < ActiveRecord::Base + belongs_to :chef +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index c2f6d492d8..312caef604 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -4,6 +4,7 @@ class Ship < ActiveRecord::Base belongs_to :pirate belongs_to :update_only_pirate, :class_name => 'Pirate' has_many :parts, :class_name => 'ShipPart' + has_many :treasures accepts_nested_attributes_for :parts, :allow_destroy => true accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb index b6a8a506b4..05c65f8a4a 100644 --- a/activerecord/test/models/ship_part.rb +++ b/activerecord/test/models/ship_part.rb @@ -2,6 +2,7 @@ class ShipPart < ActiveRecord::Base belongs_to :ship has_many :trinkets, :class_name => "Treasure", :as => :looter accepts_nested_attributes_for :trinkets, :allow_destroy => true + accepts_nested_attributes_for :ship validates_presence_of :name -end
\ No newline at end of file +end diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index a69d3fd3df..ffc65466d5 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 + belongs_to :ship has_many :price_estimates, :as => :estimate_of has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index 23cd2e0e1c..f5dc93e994 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -2,3 +2,7 @@ class User < ActiveRecord::Base has_secure_token has_secure_token :auth_token end + +class UserWithNotification < User + after_create -> { Notification.create! message: "A new user has been created." } +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index a9a6514c9d..52d3290c84 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -35,20 +40,7 @@ BEGIN END SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS collation_tests; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS enum_tests; -SQL + ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index f2cffca52c..90f5a60d7b 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -46,20 +51,7 @@ BEGIN END SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS collation_tests; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS enum_tests; -SQL + ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 55360b9aa2..df0362573b 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,8 +1,19 @@ ActiveRecord::Schema.define do + enable_extension!('uuid-ossp', ActiveRecord::Base.connection) + + create_table :uuid_parents, id: :uuid, force: true do |t| + t.string :name + end + + create_table :uuid_children, id: :uuid, force: true do |t| + t.string :name + t.uuid :uuid_parent_id + end + %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + drop_table table_name, if_exists: true end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -88,19 +99,14 @@ _SQL end end - begin - execute <<_SQL - CREATE TABLE postgresql_xml_data_type ( - id SERIAL PRIMARY KEY, - data xml - ); -_SQL - rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table - end - # This table is to verify if the :limit option is being ignored for text and binary columns create_table :limitless_fields, force: true do |t| t.binary :binary, limit: 100_000 t.text :text, limit: 100_000 end + + 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 21b23d8e0c..dffccc9326 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 ActiveRecord::Schema.define do def except(adapter_names_to_exclude) @@ -7,20 +6,6 @@ ActiveRecord::Schema.define do end end - #put adapter specific setup here - case adapter_name - when "PostgreSQL" - enable_extension!('uuid-ossp', ActiveRecord::Base.connection) - create_table :uuid_parents, id: :uuid, force: true do |t| - t.string :name - end - create_table :uuid_children, id: :uuid, force: true do |t| - t.string :name - t.uuid :uuid_parent_id - end - end - - # ------------------------------------------------------------------- # # # # Please keep these create table statements in alphabetical order # @@ -115,6 +100,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| @@ -281,6 +270,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 @@ -469,6 +463,10 @@ ActiveRecord::Schema.define do t.string :name end + create_table :notifications, force: true do |t| + t.string :message + end + create_table :numeric_data, force: true do |t| t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :big_bank_balance, precision: 15, scale: 2 @@ -623,7 +621,17 @@ ActiveRecord::Schema.define do t.string :type end - create_table :randomly_named_table, force: true do |t| + create_table :randomly_named_table1, force: true do |t| + t.string :some_attribute + t.integer :another_attribute + end + + create_table :randomly_named_table2, force: true do |t| + t.string :some_attribute + t.integer :another_attribute + end + + create_table :randomly_named_table3, force: true do |t| t.string :some_attribute t.integer :another_attribute end @@ -667,6 +675,7 @@ ActiveRecord::Schema.define do create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id + t.datetime :updated_at end create_table :speedometers, force: true, id: false do |t| @@ -772,6 +781,7 @@ ActiveRecord::Schema.define do t.column :type, :string t.column :looter_id, :integer t.column :looter_type, :string + t.belongs_to :ship end create_table :tyres, force: true do |t| @@ -871,6 +881,10 @@ ActiveRecord::Schema.define do t.string :employable_type t.integer :department_id end + create_table :recipes, force: true do |t| + t.integer :chef_id + t.integer :hotel_id + end create_table :records, force: true do |t| end diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml new file mode 100644 index 0000000000..20b128db9c --- /dev/null +++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml @@ -0,0 +1,22 @@ +--- !ruby/object:Topic + attributes: + id: + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16 14:28:11.223300000 Z + bonus_time: 2000-01-01 14:28:00.000000000 Z + last_read: 2004-04-15 + content: | + --- + :omg: :lol + important: + approved: false + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: 2015-03-10 17:05:42.000000000 Z + updated_at: 2015-03-10 17:05:42.000000000 Z diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml new file mode 100644 index 0000000000..b3d3b33141 --- /dev/null +++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml @@ -0,0 +1,182 @@ +--- !ruby/object:Topic +raw_attributes: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: '2003-07-16 14:28:11.223300' + bonus_time: '2005-01-30 14:28:00.000000' + last_read: '2004-04-15' + content: | + --- Have a nice day + ... + important: + approved: f + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: '2015-03-10 17:44:41' + updated_at: '2015-03-10 17:44:41' +attributes: !ruby/object:ActiveRecord::AttributeSet + attributes: !ruby/object:ActiveRecord::LazyAttributeHash + types: + id: &5 !ruby/object:ActiveRecord::Type::Integer + precision: + scale: + limit: + range: !ruby/range + begin: -2147483648 + end: 2147483648 + excl: true + title: &6 !ruby/object:ActiveRecord::Type::String + precision: + scale: + limit: 250 + author_name: &1 !ruby/object:ActiveRecord::Type::String + precision: + scale: + limit: + author_email_address: *1 + written_on: &4 !ruby/object:ActiveRecord::Type::DateTime + precision: + scale: + limit: + bonus_time: &7 !ruby/object:ActiveRecord::Type::Time + precision: + scale: + limit: + last_read: &8 !ruby/object:ActiveRecord::Type::Date + precision: + scale: + limit: + content: !ruby/object:ActiveRecord::Type::Serialized + coder: &9 !ruby/object:ActiveRecord::Coders::YAMLColumn + object_class: !ruby/class 'Object' + subtype: &2 !ruby/object:ActiveRecord::Type::Text + precision: + scale: + limit: + important: *2 + approved: &10 !ruby/object:ActiveRecord::Type::Boolean + precision: + scale: + limit: + replies_count: &3 !ruby/object:ActiveRecord::Type::Integer + precision: + scale: + limit: + range: !ruby/range + begin: -2147483648 + end: 2147483648 + excl: true + unique_replies_count: *3 + parent_id: *3 + parent_title: *1 + type: *1 + group: *1 + created_at: *4 + updated_at: *4 + values: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: '2003-07-16 14:28:11.223300' + bonus_time: '2005-01-30 14:28:00.000000' + last_read: '2004-04-15' + content: | + --- Have a nice day + ... + important: + approved: f + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: '2015-03-10 17:44:41' + updated_at: '2015-03-10 17:44:41' + additional_types: {} + materialized: true + delegate_hash: + id: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: id + value_before_type_cast: 1 + type: *5 + title: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: title + value_before_type_cast: The First Topic + type: *6 + author_name: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: author_name + value_before_type_cast: David + type: *1 + author_email_address: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: author_email_address + value_before_type_cast: david@loudthinking.com + type: *1 + written_on: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: written_on + value_before_type_cast: '2003-07-16 14:28:11.223300' + type: *4 + bonus_time: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: bonus_time + value_before_type_cast: '2005-01-30 14:28:00.000000' + type: *7 + last_read: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: last_read + value_before_type_cast: '2004-04-15' + type: *8 + content: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: content + value_before_type_cast: | + --- Have a nice day + ... + type: !ruby/object:ActiveRecord::Type::Serialized + coder: *9 + subtype: *2 + important: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: important + value_before_type_cast: + type: *2 + approved: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: approved + value_before_type_cast: f + type: *10 + replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: replies_count + value_before_type_cast: 1 + type: *3 + unique_replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: unique_replies_count + value_before_type_cast: 0 + type: *3 + parent_id: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: parent_id + value_before_type_cast: + type: *3 + parent_title: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: parent_title + value_before_type_cast: + type: *1 + type: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: type + value_before_type_cast: + type: *1 + group: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: group + value_before_type_cast: + type: *1 + created_at: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: created_at + value_before_type_cast: '2015-03-10 17:44:41' + type: *4 + updated_at: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: updated_at + value_before_type_cast: '2015-03-10 17:44:41' + type: *4 +new_record: false diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index aa2090b897..6ebbdbc3db 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,187 @@ +* 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. + + Fixes #20339. + + *Kevin Deisz* + +* Add a bang version to `ActiveSupport::OrderedOptions` get methods which will raise + an `KeyError` if the value is `.blank?` + + Before: + + if (slack_url = Rails.application.secrets.slack_url).present? + # Do something worthwhile + else + # Raise as important secret password is not specified + end + + After: + + slack_url = Rails.application.secrets.slack_url! + + *Aditya Sanghi*, *Gaurish Sharma* + +* Remove deprecated `Class#superclass_delegating_accessor`. + Use `Class#class_attribute` instead. + + *Akshay Vishnoi* + +* Patch `Delegator` to work with `#try`. + + Fixes #5790. + + *Nate Smith* + +* Add `Integer#positive?` and `Integer#negative?` query methods + in the vein of `Fixnum#zero?`. + + This makes it nicer to do things like `bunch_of_numbers.select(&:positive?)`. + + *DHH* + +* Encoding `ActiveSupport::TimeWithZone` to YAML now preserves the timezone information. + + Fixes #9183. + + *Andrew White* + +* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if + from a given timezone. + + *Paul A Jungwirth* + +* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if + an unrecognized callback is removed. + + *Iain Beeston* + +* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. + + Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its + contents: + + variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + + variants.phone? # => true + variants.tablet? # => true + variants.desktop? # => false + + variants.any?(:phone, :tablet) # => true + variants.any?(:phone, :desktop) # => true + variants.any?(:desktop, :watch) # => false + + `Array#inquiry` is a shortcut for wrapping the receiving array in an + `ArrayInquirer`. + + *George Claghorn* + +* Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in + Ruby 2.0. + + *Kir Shatrov* + +* Added `#without` on `Enumerable` and `Array` to return a copy of an + enumerable without the specified elements. + + *Todd Bealmear* + +* Fixed a problem where `String#truncate_words` would get stuck with a complex + string. + + *Henrik Nygren* + +* Fixed a roundtrip problem with `AS::SafeBuffer` where primitive-like strings + will be dumped as primitives: + + Before: + + YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" + YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => true + YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => false + YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => 1 + YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => 1.1 + + After: + + YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" + YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => "true" + YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => "false" + YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => "1" + YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => "1.1" + + *Godfrey Chan* + +* Enable `number_to_percentage` to keep the number's precision by allowing + `:precision` to be `nil`. + + *Jack Xu* + +* `config_accessor` became a private method, as with Ruby's `attr_accessor`. + + *Akira Matsuda* + +* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as + `Time.now` and `Date.today`. + + *Yuki Nishijima* + +* Add `file_fixture` to `ActiveSupport::TestCase`. + It provides a simple mechanism to access sample files in your test cases. + + By default file fixtures are stored in `test/fixtures/files`. This can be + configured per test-case using the `file_fixture_path` class attribute. + + *Yves Senn* + +* Return value of yielded block in `File.atomic_write`. + + *Ian Ker-Seymer* + +* Duplicate frozen array when assigning it to a `HashWithIndifferentAccess` so + that it doesn't raise a `RuntimeError` when calling `map!` on it in `convert_value`. + + Fixes #18550. + + *Aditya Kapoor* + +* Add missing time zone definitions for Russian Federation and sync them + with `zone.tab` file from tzdata version 2014j (latest). + + *Andrey Novikov* + +* Add `SecureRandom.base58` for generation of random base58 strings. + + *Matthew Draper*, *Guillermo Iguaran* + +* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and + `#tomorrow` for `Date`, `Time`, and `DateTime`. + + *George Claghorn* + +* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, + and `DateTime`. + + *George Claghorn* + +* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, + `Time`, and `DateTime`. + + `#on_weekend?` returns `true` if the receiving date/time falls on a Saturday + or Sunday. + + `#next_weekday` returns a new date/time representing the next day that does + not fall on a Saturday or Sunday. + + `#prev_weekday` returns a new date/time representing the previous day that + does not fall on a Saturday or Sunday. + + *George Claghorn* + * Change the default test order from `:sorted` to `:random`. *Rafael Mendonça França* @@ -41,13 +225,13 @@ `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will either not work at all or display a deprecation warning. -* Add Callbacks::CallbackChain.halt_and_display_warning_on_return_false +* Add `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` - to true will let an app support the deprecated way of halting callback + to `true` will let an app support the deprecated way of halting callback chains by returning `false`. - Setting the value to false will tell the app to ignore any `false` value + Setting the value to `false` will tell the app to ignore any `false` value returned by callbacks, and only halt the chain upon `throw(:abort)`. The value can also be set with the Rails configuration option @@ -60,7 +244,7 @@ *claudiob* -* Changes arguments and default value of CallbackChain's :terminator option +* Changes arguments and default value of CallbackChain's `:terminator` option Chains of callbacks defined without an explicit `:terminator` option will now be halted as soon as a `before_` callback throws `:abort`. @@ -81,6 +265,8 @@ * Add support for error dispatcher classes in `ActiveSupport::Rescuable`. Now it acts closer to Ruby's rescue. + Example: + class BaseController < ApplicationController module ErrorDispatcher def self.===(other) diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index a6424a353a..cd72f53821 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -10,7 +10,7 @@ outside of Rails. The latest version of Active Support can be installed with RubyGems: - % [sudo] gem install activesupport + % gem install activesupport Source code can be downloaded as part of the Rails project on GitHub: diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 7c40df8dc8..81c242d4b1 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' task :default => :test Rake::TestTask.new do |t| @@ -17,16 +16,3 @@ namespace :test do end or raise "Failures" end end - -spec = eval(File.read('activesupport.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index a5339e6475..12c6a4d449 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.' s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' 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.rb b/activesupport/lib/active_support.rb index 290920dbf8..588d6c49f9 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -59,6 +59,7 @@ module ActiveSupport autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini + autoload :ArrayInquirer end autoload :Rescuable @@ -72,6 +73,14 @@ module ActiveSupport end cattr_accessor :test_order # :nodoc: + + def self.halt_callback_chains_on_return_false + Callbacks::CallbackChain.halt_and_display_warning_on_return_false + end + + def self.halt_callback_chains_on_return_false=(value) + Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb new file mode 100644 index 0000000000..e7188d7adb --- /dev/null +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -0,0 +1,44 @@ +module ActiveSupport + # Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check + # its string-like contents: + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.phone? # => true + # variants.tablet? # => true + # variants.desktop? # => false + class ArrayInquirer < Array + # Passes each element of +candidates+ collection to ArrayInquirer collection. + # The method returns true if at least one element is the same. If +candidates+ + # collection is not given, method returns true. + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.any? # => true + # variants.any?(:phone, :tablet) # => true + # variants.any?('phone', 'desktop') # => true + # variants.any?(:desktop, :watch) # => false + def any?(*candidates, &block) + if candidates.none? + super + else + candidates.any? do |candidate| + include?(candidate) || include?(candidate.to_sym) + end + end + end + + private + def respond_to_missing?(name, include_private = false) + name[-1] == '?' + end + + def method_missing(name, *args) + if name[-1] == '?' + any?(name[0..-2]) + else + super + end + end + end +end 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/cache.rb b/activesupport/lib/active_support/cache.rb index 3a1e1ac3a1..837974bc85 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -325,19 +325,22 @@ module ActiveSupport def read_multi(*names) options = names.extract_options! options = merged_options(options) - results = {} - names.each do |name| - key = namespaced_key(name, options) - entry = read_entry(key, options) - if entry - if entry.expired? - delete_entry(key, options) - else - results[name] = entry.value + + instrument_multi(:read, names, options) do |payload| + results = {} + names.each do |name| + key = namespaced_key(name, options) + entry = read_entry(key, options) + if entry + if entry.expired? + delete_entry(key, options) + else + results[name] = entry.value + end end end + results end - results end # Fetches data from the cache, using the given keys. If there is data in @@ -350,8 +353,11 @@ module ActiveSupport # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") - # cache.fetch_multi("bim", "boom") { |key| key * 2 } - # # => { "bam" => "bam", "boom" => "boomboom" } + # cache.fetch_multi("bim", "unknown_key") do |key| + # "Fallback value for key: #{key}" + # end + # # => { "bim" => "bam", + # # "unknown_key" => "Fallback value for key: unknown_key" } # def fetch_multi(*names) options = names.extract_options! @@ -527,16 +533,27 @@ module ActiveSupport end def instrument(operation, key, options = nil) - log(operation, key, options) + log { "Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}" } payload = { :key => key } payload.merge!(options) if options.is_a?(Hash) ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end - def log(operation, key, options = nil) + def instrument_multi(operation, keys, options = nil) + log do + formatted_keys = keys.map { |k| "- #{k}" }.join("\n") + "Caches multi #{operation}:\n#{formatted_keys}#{options.blank? ? "" : " (#{options.inspect})"}" + end + + payload = { key: keys } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}_multi.active_support", payload) { yield(payload) } + end + + def log return unless logger && logger.debug? && !silence? - logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") + logger.debug(yield) end def find_cached_entry(key, name, options) @@ -549,9 +566,9 @@ module ActiveSupport def handle_expired_entry(entry, key, options) if entry && entry.expired? race_ttl = options[:race_condition_ttl].to_i - if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl) - # When an entry has :race_condition_ttl defined, put the stale entry back into the cache - # for a brief period while the entry is begin recalculated. + if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl) + # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache + # for a brief period while the entry is being recalculated. entry.expires_at = Time.now + race_ttl write_entry(key, entry, :expires_in => race_ttl * 2) else diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d08ecd2f7d..e6a8b84214 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -29,6 +29,7 @@ module ActiveSupport def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) + rescue Errno::ENOENT end # Preemptively iterates through all stored keys and removes the ones which have expired. diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 61b4f0b8b0..47133bf550 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -26,7 +26,14 @@ module ActiveSupport class MemCacheStore < Store ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n - def self.build_mem_cache(*addresses) + # Creates a new Dalli::Client instance with specified addresses and options. + # By default address is equal localhost:11211. + # + # ActiveSupport::Cache::MemCacheStore.build_mem_cache + # # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil> + # ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290') + # # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil> + def self.build_mem_cache(*addresses) # :nodoc: addresses = addresses.flatten options = addresses.extract_options! addresses = ["localhost:11211"] if addresses.empty? @@ -66,14 +73,17 @@ module ActiveSupport def read_multi(*names) options = names.extract_options! options = merged_options(options) - keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}] - raw_values = @data.get_multi(keys_to_names.keys, :raw => true) - values = {} - raw_values.each do |key, value| - entry = deserialize_entry(value) - values[keys_to_names[key]] = entry.value unless entry.expired? + + instrument_multi(:read, names, options) do + keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}] + raw_values = @data.get_multi(keys_to_names.keys, :raw => true) + values = {} + raw_values.each do |key, value| + entry = deserialize_entry(value) + values[keys_to_names[key]] = entry.value unless entry.expired? + end + values end - values end # Increment a cached value. This method uses the memcached incr atomic diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 8a0523d0e2..90bb2c38c3 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -126,7 +126,7 @@ module ActiveSupport PER_ENTRY_OVERHEAD = 240 - def cached_size(key, entry) + def cached_size(key, entry) # :nodoc: key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 73c6b3cb88..fe5bc82c30 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -39,7 +39,7 @@ module ActiveSupport @data = {} end - # Don't allow synchronizing since it isn't thread safe, + # Don't allow synchronizing since it isn't thread safe. def synchronize # :nodoc: yield end @@ -120,7 +120,7 @@ module ActiveSupport super end - def set_cache_value(value, name, amount, options) + def set_cache_value(value, name, amount, options) # :nodoc: if local_cache local_cache.mute do if value diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 0f1de8b076..e8ab3a7db5 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' require 'thread' module ActiveSupport @@ -79,14 +80,10 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - send "_run_#{kind}_callbacks", &block - end - - private + callbacks = send("_#{kind}_callbacks") - def _run_callbacks(callbacks, &block) if callbacks.empty? - block.call if block + yield if block_given? else runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) @@ -94,6 +91,8 @@ 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. @@ -291,13 +290,11 @@ module ActiveSupport if !halted && user_conditions.all? { |c| c.call(target, value) } user_callback.call(target, value) { - env = run.call env - env.value + run.call.value } - env else - run.call env + run.call end end end @@ -309,11 +306,10 @@ module ActiveSupport value = env.value if env.halted - run.call env + run.call else user_callback.call(target, value) { - env = run.call env - env.value + run.call.value } env end @@ -328,12 +324,11 @@ module ActiveSupport if user_conditions.all? { |c| c.call(target, value) } user_callback.call(target, value) { - env = run.call env - env.value + run.call.value } env else - run.call env + run.call end end end @@ -342,8 +337,7 @@ module ActiveSupport def self.simple(callback_sequence, user_callback) callback_sequence.around do |env, &run| user_callback.call(env.target, env.value) { - env = run.call env - env.value + run.call.value } env end @@ -373,14 +367,14 @@ module ActiveSupport def filter; @key; end def raw_filter; @filter; end - def merge(chain, new_options) + def merge_conditional_options(chain, if_option:, unless_option:) options = { :if => @if.dup, :unless => @unless.dup } - options[:if].concat Array(new_options.fetch(:unless, [])) - options[:unless].concat Array(new_options.fetch(:if, [])) + options[:if].concat Array(unless_option) + options[:unless].concat Array(if_option) self.class.build chain, @filter, @kind, options end @@ -495,17 +489,17 @@ module ActiveSupport end def around(&around) - CallbackSequence.new do |*args| - around.call(*args) { - self.call(*args) + CallbackSequence.new do |arg| + around.call(arg) { + self.call(arg) } end end - def call(*args) - @before.each { |b| b.call(*args) } - value = @call.call(*args) - @after.each { |a| a.call(*args) } + def call(arg) + @before.each { |b| b.call(arg) } + value = @call.call(arg) + @after.each { |a| a.call(arg) } value end end @@ -644,7 +638,7 @@ module ActiveSupport # set_callback :save, :after, :after_meth, if: :condition # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # - # The second arguments indicates whether the callback is to be run +:before+, + # The second argument indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This # means the first example above can also be written as: # @@ -667,10 +661,12 @@ module ActiveSupport # # ===== Options # - # * <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, a string or an array of symbols and strings, + # each naming an instance method or a proc; the callback will be called + # only when they all return a true value. + # * <tt>:unless</tt> - A symbol, a string or an array of symbols and + # strings, each naming an instance method or a proc; the callback will + # be called only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) @@ -693,19 +689,27 @@ module ActiveSupport # class Writer < Person # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end + # + # An <tt>ArgumentError</tt> will be raised if the callback has not + # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| filters.each do |filter| - filter = chain.find {|c| c.matches?(type, filter) } + callback = chain.find {|c| c.matches?(type, filter) } + + if !callback && options[:raise] + raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined" + end - if filter && options.any? - new_filter = filter.merge(chain, options) - chain.insert(chain.index(filter), new_filter) + if callback && (options.key?(:if) || options.key?(:unless)) + new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) + chain.insert(chain.index(callback), new_callback) end - chain.delete(filter) + chain.delete(callback) end target.set_callbacks name, chain end @@ -746,8 +750,8 @@ module ActiveSupport # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By - # default after callbacks executed no matter if callback chain was - # terminated or not. Option makes sense only when <tt>:terminator</tt> + # default after callbacks are executed no matter if callback chain was + # terminated or not. This option makes sense only when <tt>:terminator</tt> # option is specified. # # * <tt>:scope</tt> - Indicates which methods should be executed when an @@ -802,12 +806,6 @@ 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 342d3a9d52..0403eb70ca 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -114,7 +114,7 @@ module ActiveSupport return false else return false if base < self - @_dependencies.each { |dep| base.send(:include, dep) } + @_dependencies.each { |dep| base.include(dep) } super base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block) @@ -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/configurable.rb b/activesupport/lib/active_support/configurable.rb index 3dd44e32d8..8256c325af 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -122,6 +122,7 @@ module ActiveSupport send("#{name}=", yield) if block_given? end end + private :config_accessor end # Reads and writes attributes from a configuration <tt>OrderedHash</tt>. diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 7d0c1e4c8d..7551551bd7 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -4,3 +4,4 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/prepend_and_append' +require 'active_support/core_ext/array/inquiry' diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 45b89d2705..3177d8498e 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -21,12 +21,24 @@ class Array # %w( a b c ).to(-10) # => [] def to(position) if position >= 0 - first position + 1 + take position + 1 else self[0..position] end end + # Returns a copy of the Array without the specified elements. + # + # people = ["David", "Rafael", "Aaron", "Todd"] + # people.without "Aaron", "Todd" + # => ["David", "Rafael"] + # + # Note: This is an optimization of `Enumerable#without` that uses `Array#-` + # instead of `Array#reject` for performance reasons. + def without(*elements) + self - elements + end + # Equal to <tt>self[1]</tt>. # # %w( a b c d e ).second # => "b" diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 080e3b5ef7..d80df21e7d 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -74,7 +74,7 @@ class Array when 0 '' when 1 - self[0].to_s.dup + "#{self[0]}" when 2 "#{self[0]}#{options[:two_words_connector]}#{self[1]}" else diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb new file mode 100644 index 0000000000..e8f44cc378 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb @@ -0,0 +1,17 @@ +require 'active_support/array_inquirer' + +class Array + # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way + # to check its string-like contents. + # + # pets = [:cat, :dog].inquiry + # + # pets.cat? # => true + # pets.ferret? # => false + # + # pets.any?(:cat, :ferret) # => true + # pets.any?(:ferret, :alligator) # => false + def inquiry + ActiveSupport::ArrayInquirer.new(self) + end +end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 152eb02218..b611d34c27 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -3,7 +3,7 @@ class Array # # Specifically: # - # * If the argument is +nil+ an empty list is returned. + # * If the argument is +nil+ an empty array is returned. # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. # * Otherwise, returns an array with the argument as its single element. # @@ -15,12 +15,13 @@ class Array # # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns - # +nil+ right away. + # an array with the argument as its single element right away. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. - # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+. + # * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+ + # it returns an array with the argument as its single element. # - # The second point is easily explained with some enumerables: + # The last point is easily explained with some enumerables: # # Array(foo: :bar) # => [[:foo, :bar]] # Array.wrap(foo: :bar) # => [{:foo=>:bar}] diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index c750a10bb2..ef903d59b5 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,3 +1,2 @@ require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/class/subclasses' diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb deleted file mode 100644 index 1c305c5970..0000000000 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/module/deprecation' - - -class Class - def superclass_delegating_accessor(name, options = {}) - # Create private _name and _name= methods that can still be used if the public - # methods are overridden. - _superclass_delegating_accessor("_#{name}", options) - - # Generate the public methods name, name=, and name?. - # These methods dispatch to the private _name, and _name= methods, making them - # overridable. - singleton_class.send(:define_method, name) { send("_#{name}") } - singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } - singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - - # If an instance_reader is needed, generate public instance methods name and name?. - if options[:instance_reader] != false - define_method(name) { send("_#{name}") } - define_method("#{name}?") { !!send("#{name}") } - end - end - - deprecate superclass_delegating_accessor: :class_attribute - - private - # Take the object being set and store it in a method. This gives us automatic - # inheritance behavior, without having to store the object in an instance - # variable and look up the superclass chain manually. - def _stash_object_in_method(object, method, instance_reader = true) - singleton_class.remove_possible_method(method) - singleton_class.send(:define_method, method) { object } - remove_possible_method(method) - define_method(method) { object } if instance_reader - end - - def _superclass_delegating_accessor(name, options = {}) - singleton_class.send(:define_method, "#{name}=") do |value| - _stash_object_in_method(value, name, options[:instance_reader] != false) - end - send("#{name}=", nil) - end -end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index df419a6e63..31479a1269 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -35,6 +35,7 @@ class Date # date.to_s(:db) # => "2007-11-10" # # date.to_formatted_s(:short) # => "10 Nov" + # date.to_formatted_s(:number) # => "20071110" # date.to_formatted_s(:long) # => "November 10, 2007" # date.to_formatted_s(:long_ordinal) # => "November 10th, 2007" # date.to_formatted_s(:rfc822) # => "10 Nov 2007" @@ -82,6 +83,11 @@ class Date ::Time.send(form, year, month, day) end + # Returns a string which represents the time in used time zone as DateTime + # defined by XML Schema: + # + # date = Date.new(2015, 05, 23) # => Sat, 23 May 2015 + # date.xmlschema # => "2015-05-23T00:00:00+04:00" def xmlschema in_time_zone.xmlschema 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 b85e49aca5..01153606c9 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 @@ -9,15 +9,26 @@ module DateAndTime :saturday => 5, :sunday => 6 } + WEEKEND_DAYS = [ 6, 0 ] # Returns a new date/time representing yesterday. def yesterday - advance(:days => -1) + advance(days: -1) + end + + # Returns a new date/time representing the previous day. + def prev_day + advance(days: -1) end # Returns a new date/time representing tomorrow. def tomorrow - advance(:days => 1) + advance(days: 1) + end + + # Returns a new date/time representing the next day. + def next_day + advance(days: 1) end # Returns true if the date/time is today. @@ -35,6 +46,11 @@ module DateAndTime self > self.class.current end + # Returns true if the date/time falls on a Saturday or Sunday. + def on_weekend? + WEEKEND_DAYS.include?(wday) + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(:days => -days) @@ -109,11 +125,33 @@ module DateAndTime alias :at_beginning_of_year :beginning_of_year # Returns a new date/time representing the given day in the next week. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week # => Mon, 11 May 2015 + # # The +given_day_in_next_week+ defaults to the beginning of the week # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ - # when set. +DateTime+ objects have their time set to 0:00. - def next_week(given_day_in_next_week = Date.beginning_of_week) - first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) + # when set. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week(:friday) # => Fri, 15 May 2015 + # + # +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 + 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 + end + + # Returns a new date/time representing the next weekday. + def next_weekday + if next_day.on_weekend? + next_week(:monday, same_time: true) + else + next_day + end end # Short-hand for months_since(1). @@ -134,12 +172,23 @@ module DateAndTime # Returns a new date/time representing the given day in the previous week. # Week is assumed to start on +start_day+, default is # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. - # DateTime objects have their time set to 0:00. - def prev_week(start_day = Date.beginning_of_week) - first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) + # DateTime objects have their time set to 0:00 unless +same_time+ is true. + def prev_week(start_day = Date.beginning_of_week, same_time: false) + result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) + same_time ? copy_time_to(result) : result end alias_method :last_week, :prev_week + # Returns a new date/time representing the previous weekday. + def prev_weekday + if prev_day.on_weekend? + copy_time_to(beginning_of_week(:friday)) + else + prev_day + end + end + alias_method :last_weekday, :prev_weekday + # Short-hand for months_ago(1). def prev_month months_ago(1) @@ -235,17 +284,20 @@ module DateAndTime end private + def first_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time + end - def first_hour(date_or_time) - date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time - end + def last_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time + end - def last_hour(date_or_time) - date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time - end + def days_span(day) + (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 + end - def days_span(day) - (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 - end + def copy_time_to(other) + other.change(hour: hour, min: min, sec: sec, usec: try(:usec)) + end end end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 1343beb87a..fc7531d088 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -60,6 +60,32 @@ module Enumerable def exclude?(object) !include?(object) end + + # Returns a copy of the enumerable without the specified elements. + # + # ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd" + # => ["David", "Rafael"] + # + # {foo: 1, bar: 2, baz: 3}.without :bar + # => {foo: 1, baz: 3} + def without(*elements) + reject { |element| elements.include?(element) } + end + + # Convert an enumerable to an array based on the given key. + # + # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) + # => ["David", "Rafael", "Aaron"] + # + # [{ 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 class Range #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 38374af388..fad6fa8d9d 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -20,7 +20,7 @@ class File temp_file = Tempfile.new(basename(file_name), temp_dir) temp_file.binmode - yield temp_file + return_val = yield temp_file temp_file.close if File.exist?(file_name) @@ -40,6 +40,9 @@ class File chown(old_stat.uid, old_stat.gid, file_name) # This operation will affect filesystem ACL's chmod(old_stat.mode, file_name) + + # Make sure we return the result of the yielded block + return_val rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 9297a59c46..c30044b9ff 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -14,7 +14,7 @@ class Hash result end - # Destructively convert all keys using the block operations. + # Destructively converts all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! return enum_for(:transform_keys!) unless block_given? @@ -34,7 +34,7 @@ class Hash transform_keys(&:to_s) end - # Destructively convert all keys to strings. Same as + # Destructively converts all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! transform_keys!(&:to_s) @@ -52,14 +52,14 @@ class Hash end alias_method :to_options, :symbolize_keys - # Destructively convert all keys to symbols, as long as they respond + # Destructively converts all keys to symbols, as long as they respond # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. def symbolize_keys! transform_keys!{ |key| key.to_sym rescue key } end alias_method :to_options!, :symbolize_keys! - # Validate all keys in a hash match <tt>*valid_keys</tt>, raising + # Validates all keys in a hash match <tt>*valid_keys</tt>, raising # ArgumentError on a mismatch. # # Note that keys are treated differently than HashWithIndifferentAccess, @@ -89,7 +89,7 @@ class Hash _deep_transform_keys_in_object(self, &block) end - # Destructively convert all keys by using the block operation. + # Destructively converts all keys by using the block operation. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_transform_keys!(&block) @@ -108,7 +108,7 @@ class Hash deep_transform_keys(&:to_s) end - # Destructively convert all keys to strings. + # Destructively converts all keys to strings. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_stringify_keys! @@ -127,7 +127,7 @@ class Hash deep_transform_keys{ |key| key.to_sym rescue key } end - # Destructively convert all keys to symbols, as long as they respond + # Destructively converts all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all # nested hashes and arrays. def deep_symbolize_keys! diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 41b2279013..1d5f38231a 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,5 +1,5 @@ class Hash - # Slice a hash to include only the given keys. Returns a hash containing + # Slices a hash to include only the given keys. Returns a hash containing # the given keys. # # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b) diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 82080ffe51..f0b7382ef3 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -17,21 +17,6 @@ class Integer # # # equivalent to Time.now.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples - # above, care should be taken to note that this is not true if the result of - # +months+, +years+, etc is converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def months ActiveSupport::Duration.new(self * 30.days, [[:months, self]]) end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index eb44646848..9189e6d977 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,4 +1,3 @@ -require 'rbconfig' require 'tempfile' module Kernel diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index 56c79c04bd..20a0856e71 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,9 +1,7 @@ -require 'active_support/core_ext/module/aliasing' - -module Marshal - class << self - def load_with_autoloading(source) - load_without_autoloading(source) +module ActiveSupport + module MarshalWithAutoloading # :nodoc: + def load(source) + super(source) rescue ArgumentError, NameError => exc if exc.message.match(%r|undefined class/module (.+)|) # try loading the class/module @@ -15,7 +13,7 @@ module Marshal raise exc end end - - alias_method_chain :load, :autoloading end end + +Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading) diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 0a6fadf928..b6934b9c54 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -1,4 +1,7 @@ class Module + # NOTE: This method is deprecated. Please use <tt>Module#prepend</tt> that + # comes with Ruby 2.0 or newer instead. + # # Encapsulates the common pattern of: # # alias_method :foo_without_feature, :foo @@ -21,6 +24,8 @@ class Module # # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) + ActiveSupport::Deprecation.warn("alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.") + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 @@ -43,7 +48,7 @@ class Module end # Allows you to make aliases for attributes, which includes - # getter, setter, and query methods. + # getter, setter, and a predicate. # # class Content < ActiveRecord::Base # # has a title attribute diff --git a/activesupport/lib/active_support/core_ext/module/anonymous.rb b/activesupport/lib/active_support/core_ext/module/anonymous.rb index b0c7b021db..0ecc67a855 100644 --- a/activesupport/lib/active_support/core_ext/module/anonymous.rb +++ b/activesupport/lib/active_support/core_ext/module/anonymous.rb @@ -7,6 +7,13 @@ class Module # m = Module.new # m.name # => nil # + # +anonymous?+ method returns true if module does not have a name: + # + # Module.new.anonymous? # => true + # + # module M; end + # M.anonymous? # => false + # # A module gets a name when it is first assigned to a constant. Either # via the +module+ or +class+ keyword or by an explicit assignment: # diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 07a392404e..e26b594fc4 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -63,10 +63,10 @@ class Module # # == Mix-in noise exiled to its own file: # - # Once our chunk of behavior starts pushing the scroll-to-understand it's + # Once our chunk of behavior starts pushing the scroll-to-understand-it # boundary, we give in and move it to a separate file. At this size, the - # overhead feels in good proportion to the size of our extraction, despite - # diluting our at-a-glance sense of how things really work. + # increased overhead can be a reasonable tradeoff even if it reduces our + # at-a-glance perception of how things work. # # class Todo # # Other todo implementation diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 24df83800b..9b7a429db9 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.first.split(':', 2) + file, line = caller(1, 1).first.split(':', 2) line = line.to_i to = to.to_s @@ -185,19 +185,31 @@ class Module # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. - - exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - - method_def = [ - "def #{method_prefix}#{method}(#{definition})", - " _ = #{to}", - " if !_.nil? || nil.respond_to?(:#{method})", - " _.#{method}(#{definition})", - " else", - " #{exception unless allow_nil}", - " end", + if allow_nil + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + "_ = #{to}", + "if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + "end", "end" - ].join ';' + ].join ';' + else + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " _.#{method}(#{definition})", + "rescue NoMethodError => e", + " if _.nil? && e.name == :#{method}", + " #{exception}", + " else", + " raise", + " end", + "end" + ].join ';' + end module_eval(method_def, file, line) end diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 8a2569a7d0..52632d2c6b 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -1,13 +1,13 @@ class Module - # Remove the named method, if it exists. + # Removes the named method, if it exists. def remove_possible_method(method) if method_defined?(method) || private_method_defined?(method) undef_method(method) end end - # Replace the existing method definition, if there is one, with the contents - # of the block. + # Replaces the existing method definition, if there is one, with the passed + # block as its body. def redefine_method(method, &block) remove_possible_method(method) define_method(method, &block) diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb index b82148e4e5..6b447d772b 100644 --- a/activesupport/lib/active_support/core_ext/name_error.rb +++ b/activesupport/lib/active_support/core_ext/name_error.rb @@ -23,8 +23,7 @@ class NameError # # => true def missing_name?(name) if name.is_a? Symbol - last_name = (missing_name || '').split('::').last - last_name == name.to_s + self.name == name else missing_name == name.to_s end diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index a6bc0624be..bcdc3eace2 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' +require 'active_support/core_ext/numeric/inquiry' require 'active_support/core_ext/numeric/conversions' diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb new file mode 100644 index 0000000000..7e7ac1b0b2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb @@ -0,0 +1,26 @@ +unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3 +class Numeric + # Returns true if the number is positive. + # + # 1.positive? # => true + # 0.positive? # => false + # -1.positive? # => false + def positive? + self > 0 + end + + # Returns true if the number is negative. + # + # -1.negative? # => true + # 0.negative? # => false + # 1.negative? # => false + def negative? + self < 0 + end +end + +class Complex + undef :positive? + undef :negative? +end +end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index ef32817f55..6c4a975495 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -1,6 +1,8 @@ require 'active_support/duration' require 'active_support/core_ext/time/calculations' require 'active_support/core_ext/time/acts_like' +require 'active_support/core_ext/date/calculations' +require 'active_support/core_ext/date/acts_like' class Numeric # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. @@ -16,21 +18,6 @@ class Numeric # # # equivalent to Time.current.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples above, care - # should be taken to note that this is not true if the result of `months', `years', etc is - # converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def seconds ActiveSupport::Duration.new(self, [[:seconds, self]]) end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 0191d2e973..ad5b2af161 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -40,6 +40,7 @@ class Hash # dup[:a][:c] # => "c" def deep_dup each_with_object(dup) do |(key, value), hash| + hash.delete(key) hash[key.deep_dup] = value.deep_dup end end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 698b2d1920..0db787010c 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -9,7 +9,6 @@ require 'time' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date/conversions' -require 'active_support/core_ext/module/aliasing' # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, @@ -26,22 +25,25 @@ require 'active_support/core_ext/module/aliasing' # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} # should give exactly the same results with or without active support. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| - klass.class_eval do - def to_json_with_active_support_encoder(options = nil) + +module ActiveSupport + module ToJsonWithActiveSupportEncoder # :nodoc: + def to_json(options = nil) if options.is_a?(::JSON::State) # Called from JSON.{generate,dump}, forward it to JSON gem's to_json - self.to_json_without_active_support_encoder(options) + super(options) else # to_json is being invoked directly, use ActiveSupport's encoder ActiveSupport::JSON.encode(self, options) end end - - alias_method_chain :to_json, :active_support_encoder end end +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass| + klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder) +end + class Object def as_json(options = nil) #:nodoc: if respond_to?(:to_hash) diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index e0f70b9caa..69be6c4abc 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -1,4 +1,34 @@ +require 'delegate' + +module ActiveSupport + module Tryable #:nodoc: + def try(*a, &b) + try!(*a, &b) if a.empty? || respond_to?(a.first) + end + + def try!(*a, &b) + if a.empty? && block_given? + if b.arity.zero? + instance_eval(&b) + else + yield self + end + else + public_send(*a, &b) + end + end + end +end + class Object + include ActiveSupport::Tryable + + ## + # :method: try + # + # :call-seq: + # try(*a, &b) + # # Invokes the public method whose name goes as first argument just like # +public_send+ does, except that if the receiver does not respond to it the # call returns +nil+ rather than raising an exception. @@ -56,30 +86,40 @@ class Object # # Please also note that +try+ is defined on +Object+. Therefore, it won't work # with instances of classes that do not have +Object+ among their ancestors, - # like direct subclasses of +BasicObject+. For example, using +try+ with - # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on - # the delegator itself. - def try(*a, &b) - try!(*a, &b) if a.empty? || respond_to?(a.first) - end + # like direct subclasses of +BasicObject+. + ## + # :method: try! + # + # :call-seq: + # try!(*a, &b) + # # Same as #try, but raises a NoMethodError exception if the receiver is # not +nil+ and does not implement the tried method. # # "a".try!(:upcase) # => "A" # nil.try!(:upcase) # => nil # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum - def try!(*a, &b) - if a.empty? && block_given? - if b.arity.zero? - instance_eval(&b) - else - yield self - end - else - public_send(*a, &b) - end - end +end + +class Delegator + include ActiveSupport::Tryable + + ## + # :method: try + # + # :call-seq: + # try(a*, &b) + # + # See Object#try + + ## + # :method: try! + # + # :call-seq: + # try!(a*, &b) + # + # See Object#try! end class NilClass diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 7d38e1d134..513c8b1d55 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -7,7 +7,7 @@ class Object # provided. Each method called on the block variable must take an options # hash as its final argument. # - # Without <tt>with_options></tt>, this code contains duplication: + # Without <tt>with_options</tt>, this code contains duplication: # # class Account < ActiveRecord::Base # has_many :customers, dependent: :destroy diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index b1a12781f3..83eced50bf 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -3,9 +3,24 @@ class Range :db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" } } - # Gives a human readable format of the range. + # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. # - # (1..100).to_formatted_s # => "1..100" + # This method is aliased to <tt>to_s</tt>. + # + # range = (1..100) # => 1..100 + # + # range.to_formatted_s # => "1..100" + # range.to_s # => "1..100" + # + # range.to_formatted_s(:db) # => "BETWEEN '1' AND '100'" + # range.to_s(:db) # => "BETWEEN '1' AND '100'" + # + # == Adding your own range formats to to_formatted_s + # You can add your own formats to the Range::RANGE_FORMATS hash. + # Use the format name as the hash key and a Proc instance. + # + # # config/initializers/range_formats.rb + # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } def to_formatted_s(format = :default) if formatter = RANGE_FORMATS[format] formatter.call(first, last) diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb index ecef78f55f..dc6dad5ced 100644 --- a/activesupport/lib/active_support/core_ext/range/each.rb +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -1,23 +1,21 @@ -require 'active_support/core_ext/module/aliasing' +module ActiveSupport + module EachTimeWithZone #:nodoc: + def each(&block) + ensure_iteration_allowed + super + end -class Range #:nodoc: + def step(n = 1, &block) + ensure_iteration_allowed + super + end - def each_with_time_with_zone(&block) - ensure_iteration_allowed - each_without_time_with_zone(&block) - end - alias_method_chain :each, :time_with_zone + private - def step_with_time_with_zone(n = 1, &block) - ensure_iteration_allowed - step_without_time_with_zone(n, &block) - end - alias_method_chain :step, :time_with_zone - - private - def ensure_iteration_allowed - if first.is_a?(Time) - raise TypeError, "can't iterate from #{first.class}" - end + def ensure_iteration_allowed + raise TypeError, "can't iterate from #{first.class}" if first.is_a?(Time) + end end end + +Range.prepend(ActiveSupport::EachTimeWithZone) diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 3a07401c8a..c69e1e3fb9 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,23 +1,23 @@ -require 'active_support/core_ext/module/aliasing' - -class Range - # Extends the default Range#include? to support range comparisons. - # (1..5).include?(1..5) # => true - # (1..5).include?(2..3) # => true - # (1..5).include?(2..6) # => false - # - # The native Range#include? behavior is untouched. - # ('a'..'f').include?('c') # => true - # (5..9).include?(11) # => false - def include_with_range?(value) - if value.is_a?(::Range) - # 1...10 includes 1..9 but it does not include 1..10. - operator = exclude_end? && !value.exclude_end? ? :< : :<= - include_without_range?(value.first) && value.last.send(operator, last) - else - include_without_range?(value) +module ActiveSupport + module IncludeWithRange #:nodoc: + # Extends the default Range#include? to support range comparisons. + # (1..5).include?(1..5) # => true + # (1..5).include?(2..3) # => true + # (1..5).include?(2..6) # => false + # + # The native Range#include? behavior is untouched. + # ('a'..'f').include?('c') # => true + # (5..9).include?(11) # => false + def include?(value) + if value.is_a?(::Range) + # 1...10 includes 1..9 but it does not include 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.send(operator, last) + else + super + end end end - - alias_method_chain :include?, :range end + +Range.prepend(ActiveSupport::IncludeWithRange) diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb new file mode 100644 index 0000000000..6cdbea1f37 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -0,0 +1,23 @@ +require 'securerandom' + +module SecureRandom + BASE58_ALPHABET = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a - ['0', 'O', 'I', 'l'] + # SecureRandom.base58 generates a random base58 string. + # + # The argument _n_ specifies the length, of the random string to be generated. + # + # If _n_ is not specified or is nil, 16 is assumed. It may be larger in the future. + # + # The result may contain alphanumeric characters except 0, O, I and l + # + # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # + def self.base58(n = 16) + SecureRandom.random_bytes(n).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(58) if idx >= 58 + BASE58_ALPHABET[idx] + end.join + end +end diff --git a/activesupport/lib/active_support/core_ext/string/behavior.rb b/activesupport/lib/active_support/core_ext/string/behavior.rb index 4aa960039b..710f1f4670 100644 --- a/activesupport/lib/active_support/core_ext/string/behavior.rb +++ b/activesupport/lib/active_support/core_ext/string/behavior.rb @@ -1,5 +1,5 @@ class String - # Enable more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>. + # Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>. def acts_like_string? true end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 096292dc58..375ec1aef8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -17,15 +17,15 @@ class String # str.squish! # => "foo bar boo" # str # => "foo bar boo" def squish! - gsub!(/\A[[:space:]]+/, '') - gsub!(/[[:space:]]+\z/, '') gsub!(/[[:space:]]+/, ' ') + strip! self end # Returns a new string with all occurrences of the patterns removed. # str = "foo bar test" # str.remove(" test") # => "foo bar" + # str.remove(" test", /bar/) # => "foo " # str # => "foo bar test" def remove(*patterns) dup.remove!(*patterns) @@ -33,8 +33,8 @@ class String # Alters the string by removing all occurrences of the patterns. # str = "foo bar test" - # str.remove!(" test") # => "foo bar" - # str # => "foo bar" + # str.remove!(" test", /bar/) # => "foo " + # str # => "foo " def remove!(*patterns) patterns.each do |pattern| gsub! pattern, "" @@ -93,7 +93,7 @@ class String def truncate_words(words_count, options = {}) sep = options[:separator] || /\s+/ sep = Regexp.escape(sep.to_s) unless Regexp === sep - if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m $1 + (options[:omission] || '...') else dup diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 38d567c014..97f9720b2b 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -178,7 +178,7 @@ class String ActiveSupport::Inflector.tableize(self) end - # Create a class name from a plural table name like Rails does for table names to models. + # Creates a class name from a plural table name like Rails does for table names to models. # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index a124202936..7055f7f699 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/multibyte' class String @@ -36,6 +35,13 @@ class String ActiveSupport::Multibyte.proxy_class.new(self) end + # Returns +true+ if string has utf_8 encoding. + # + # utf_8_str = "some string".encode "UTF-8" + # iso_str = "some string".encode "ISO-8859-1" + # + # utf_8_str.is_utf8? # => true + # iso_str.is_utf8? # => false def is_utf8? case encoding when Encoding::UTF_8 diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index ba8d4acd6d..c676b26b06 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -13,7 +13,7 @@ class ERB # This method is also aliased as <tt>h</tt>. # # In your ERB templates, use this method to escape any unsafe content. For example: - # <%=h @person.name %> + # <%= h @person.name %> # # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? @@ -85,6 +85,11 @@ class ERB # automatically flag the result as HTML safe, since the raw value is unsafe to # use inside HTML attributes. # + # If your JSON is being used downstream for insertion into the DOM, be aware of + # whether or not it is being inserted via +html()+. Most JQuery plugins do this. + # If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated + # content returned by your JSON. + # # If you need to output JSON elsewhere in your HTML, you can just do something # like this, as any unsafe characters (including quotation marks) will be # automatically escaped for you: @@ -217,7 +222,7 @@ module ActiveSupport #:nodoc: end def encode_with(coder) - coder.represent_scalar nil, to_str + coder.represent_object nil, to_str end UNSAFE_STRING_METHODS.each do |unsafe_method| diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 649dc52865..c554501893 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -48,7 +48,11 @@ class Time alias_method :at, :at_with_coercion end - # Seconds since midnight: Time.now.seconds_since_midnight + # Returns the number of seconds since 00:00:00. + # + # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 + # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 + # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 def seconds_since_midnight to_i - change(:hour => 0).to_i + (usec / 1.0e+6) end @@ -69,7 +73,7 @@ class Time # and minute is passed, then sec, usec and nsec is set to 0. The +options+ # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> - # <tt>:nsec</tt>. Path either <tt>:usec</tt> or <tt>:nsec</tt>, not both. + # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) @@ -94,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 @@ -242,8 +246,10 @@ class Time # 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 cause it's expensive - if other.is_a?(Time) + # 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 diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 0668eadb1e..d683e7c777 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -26,7 +26,7 @@ class Time # <tt>current_user.time_zone</tt> just needs to return a string identifying the user's preferred time zone: # # class ApplicationController < ActionController::Base - # around_filter :set_time_zone + # around_action :set_time_zone # # def set_time_zone # if logged_in? 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 e03e7c30d8..664cc15a29 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -373,7 +373,7 @@ module ActiveSupport #:nodoc: # Is the provided constant path defined? def qualified_const_defined?(path) - Object.qualified_const_defined?(path.sub(/^::/, ''), false) + Object.const_defined?(path, false) end # Given +path+, a filesystem path to a ruby file, return an array of @@ -607,7 +607,7 @@ module ActiveSupport #:nodoc: def autoloaded?(desc) return false if desc.is_a?(Module) && desc.anonymous? name = to_constant_name desc - return false unless qualified_const_defined? name + return false unless qualified_const_defined?(name) return autoloaded_constants.include?(name) end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 9f9dca8453..28d2d78643 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -38,6 +38,18 @@ module ActiveSupport silence: ->(message, callstack) {}, } + # Behavior module allows to determine how to display deprecation messages. + # 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+. + # [+log+] Log all deprecation warnings to +Rails.logger+. + # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. + # [+silence+] Do nothing. + # + # Setting behaviors only affects deprecations that happen after boot time. + # 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/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index cab8a1b14d..c74e9c40ac 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -31,12 +31,14 @@ module ActiveSupport method_names += options.keys method_names.each do |method_name| - target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation| - target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block| + mod = Module.new do + define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - send(:"#{target}_without_deprecation#{punctuation}", *args, &block) + super(*args, &block) end end + + target_module.prepend(mod) end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index a03a66b96b..9b264cbb79 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -25,15 +25,17 @@ module ActiveSupport end end - # This DeprecatedObjectProxy transforms object to deprecated object. + # DeprecatedObjectProxy transforms an object into a deprecated one. It + # takes an object, a deprecation message and optionally a deprecator. The + # deprecator defaults to +ActiveSupport::Deprecator+ if none is specified. # - # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!") - # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance) + # deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated") + # # => #<Object:0x007fb9b34c34b0> # - # When someone executes any method except +inspect+ on proxy object this will - # trigger +warn+ method on +deprecator_instance+. - # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt> + # deprecated_object.to_s + # DEPRECATION WARNING: This object is now deprecated. + # (Backtrace) + # # => "#<Object:0x007fb9b34c34b0>" class DeprecatedObjectProxy < DeprecationProxy def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance) @object = object @@ -51,13 +53,16 @@ module ActiveSupport end end - # This DeprecatedInstanceVariableProxy transforms instance variable to - # deprecated instance variable. + # DeprecatedInstanceVariableProxy transforms an instance variable into a + # deprecated one. It takes an instance of a class, a method on that class + # and an instance variable. It optionally takes a deprecator as the last + # argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none + # is specified. # # class Example - # def initialize(deprecator) - # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) - # @_request = :a_request + # def initialize + # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request) + # @_request = :special_request # end # # def request @@ -69,12 +74,17 @@ module ActiveSupport # end # end # - # When someone execute any method on @request variable this will trigger - # +warn+ method on +deprecator_instance+ and will fetch <tt>@_request</tt> - # variable via +request+ method and execute the same method on non-proxy - # instance variable. + # example = Example.new + # # => #<Example:0x007fb9b31090b8 @_request=:special_request, @request=:special_request> + # + # example.old_request.to_s + # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of + # @request.to_s + # (Bactrace information…) + # "special_request" # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + # example.request.to_s + # # => "special_request" class DeprecatedInstanceVariableProxy < DeprecationProxy def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance) @instance = instance @@ -93,15 +103,23 @@ module ActiveSupport end end - # This DeprecatedConstantProxy transforms constant to deprecated constant. + # DeprecatedConstantProxy transforms a constant into a deprecated one. It + # takes the names of an old (deprecated) constant and of a new constant + # (both in string form) and optionally a deprecator. The deprecator defaults + # to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant + # now returns the value of the new one. + # + # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # - # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST') - # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance) + # (In a later update, the orignal implementation of `PLANETS` has been removed.) # - # When someone use old constant this will trigger +warn+ method on - # +deprecator_instance+. + # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) + # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + # PLANETS.map { |planet| planet.capitalize } + # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. + # (Bactrace information…) + # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) @old_const = old_const @@ -109,6 +127,11 @@ module ActiveSupport @deprecator = deprecator end + # 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') + # PLANETS.class # => Array def class target.class end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 2818b8d68b..c63b61e97a 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -52,10 +52,38 @@ module ActiveSupport end end + # Returns the amount of seconds a duration covers as a string. + # For more information check to_i method. + # + # 1.day.to_s # => "86400" def to_s @value.to_s end + # Returns the number of seconds that this Duration represents. + # + # 1.minute.to_i # => 60 + # 1.hour.to_i # => 3600 + # 1.day.to_i # => 86400 + # + # Note that this conversion makes some assumptions about the + # duration of some periods, e.g. months are always 30 days + # and years are 365.25 days: + # + # # equivalent to 30.days.to_i + # 1.month.to_i # => 2592000 + # + # # equivalent to 365.25.days.to_i + # 1.year.to_i # => 31557600 + # + # In such cases, Ruby's core + # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and + # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision + # date and time arithmetic. + def to_i + @value.to_i + end + # Returns +true+ if +other+ is also a Duration instance, which has the # same parts as this one. def eql?(other) @@ -91,7 +119,7 @@ module ActiveSupport reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. - to_sentence(:locale => :en) + to_sentence(locale: ::I18n.default_locale) end def as_json(options = nil) #:nodoc: diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 1468c62151..4f71f13971 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -267,7 +267,7 @@ module ActiveSupport value.nested_under_indifferent_access end elsif value.is_a?(Array) - unless options[:for] == :assignment + if options[:for] != :assignment || value.frozen? value = value.dup end value.map! { |e| convert_value(e, options) } diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 9e742b1917..95f3f6255a 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -68,7 +68,7 @@ module I18n end def self.include_fallbacks_module - I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + I18n.backend.class.include(I18n::Backend::Fallbacks) end def self.init_fallbacks(fallbacks) diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 74b3a7c2a9..be369d21c6 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -22,58 +22,58 @@ module ActiveSupport # pluralized using rules defined for that language. By default, # this parameter is set to <tt>:en</tt>. # - # 'post'.pluralize # => "posts" - # 'octopus'.pluralize # => "octopi" - # 'sheep'.pluralize # => "sheep" - # 'words'.pluralize # => "words" - # 'CamelOctopus'.pluralize # => "CamelOctopi" - # 'ley'.pluralize(:es) # => "leyes" + # pluralize('post') # => "posts" + # pluralize('octopus') # => "octopi" + # pluralize('sheep') # => "sheep" + # pluralize('words') # => "words" + # pluralize('CamelOctopus') # => "CamelOctopi" + # pluralize('ley', :es) # => "leyes" def pluralize(word, locale = :en) apply_inflections(word, inflections(locale).plurals) end - # The reverse of +pluralize+, returns the singular form of a word in a + # The reverse of #pluralize, returns the singular form of a word in a # string. # # If passed an optional +locale+ parameter, the word will be # singularized using rules defined for that language. By default, # this parameter is set to <tt>:en</tt>. # - # 'posts'.singularize # => "post" - # 'octopi'.singularize # => "octopus" - # 'sheep'.singularize # => "sheep" - # 'word'.singularize # => "word" - # 'CamelOctopi'.singularize # => "CamelOctopus" - # 'leyes'.singularize(:es) # => "ley" + # singularize('posts') # => "post" + # singularize('octopi') # => "octopus" + # singularize('sheep') # => "sheep" + # singularize('word') # => "word" + # singularize('CamelOctopi') # => "CamelOctopus" + # singularize('leyes', :es) # => "ley" def singularize(word, locale = :en) apply_inflections(word, inflections(locale).singulars) end - # By default, +camelize+ converts strings to UpperCamelCase. If the argument - # to +camelize+ is set to <tt>:lower</tt> then +camelize+ produces + # Converts strings to UpperCamelCase. + # If the +uppercase_first_letter+ parameter is set to false, then produces # lowerCamelCase. # - # +camelize+ will also convert '/' to '::' which is useful for converting + # Also converts '/' to '::' which is useful for converting # paths to namespaces. # - # 'active_model'.camelize # => "ActiveModel" - # 'active_model'.camelize(:lower) # => "activeModel" - # 'active_model/errors'.camelize # => "ActiveModel::Errors" - # 'active_model/errors'.camelize(:lower) # => "activeModel::Errors" + # camelize('active_model') # => "ActiveModel" + # camelize('active_model', false) # => "activeModel" + # camelize('active_model/errors') # => "ActiveModel::Errors" + # camelize('active_model/errors', false) # => "activeModel::Errors" # # As a rule of thumb you can think of +camelize+ as the inverse of - # +underscore+, though there are cases where that does not hold: + # #underscore, though there are cases where that does not hold: # - # 'SSLError'.underscore.camelize # => "SslError" + # camelize(underscore('SSLError')) # => "SslError" 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!(/\//, '::') + string.gsub!('/'.freeze, '::'.freeze) string end @@ -81,16 +81,16 @@ module ActiveSupport # # Changes '::' to '/' to convert namespaces to paths. # - # 'ActiveModel'.underscore # => "active_model" - # 'ActiveModel::Errors'.underscore # => "active_model/errors" + # underscore('ActiveModel') # => "active_model" + # underscore('ActiveModel::Errors') # => "active_model/errors" # # As a rule of thumb you can think of +underscore+ as the inverse of - # +camelize+, though there are cases where that does not hold: + # #camelize, though there are cases where that does not hold: # - # 'SSLError'.underscore.camelize # => "SslError" + # camelize(underscore('SSLError')) # => "SslError" def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ - word = camel_cased_word.to_s.gsub(/::/, '/') + 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') @@ -101,14 +101,14 @@ module ActiveSupport # Tweaks an attribute name for display to end users. # - # Specifically, +humanize+ performs these transformations: + # Specifically, performs these transformations: # - # * Applies human inflection rules to the argument. - # * Deletes leading underscores, if any. - # * Removes a "_id" suffix if present. - # * Replaces underscores with spaces, if any. - # * Downcases all words except acronyms. - # * Capitalizes the first word. + # * Applies human inflection rules to the argument. + # * Deletes leading underscores, if any. + # * Removes a "_id" suffix if present. + # * Replaces underscores with spaces, if any. + # * Downcases all words except acronyms. + # * Capitalizes the first word. # # The capitalization of the first word can be turned off by setting the # +:capitalize+ option to false (default is true). @@ -148,34 +148,34 @@ module ActiveSupport # # +titleize+ is also aliased as +titlecase+. # - # 'man from the boondocks'.titleize # => "Man From The Boondocks" - # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" - # 'TheManWithoutAPast'.titleize # => "The Man Without A Past" - # 'raiders_of_the_lost_ark'.titleize # => "Raiders Of The Lost Ark" + # titleize('man from the boondocks') # => "Man From The Boondocks" + # titleize('x-men: the last stand') # => "X Men: The Last Stand" + # titleize('TheManWithoutAPast') # => "The Man Without A Past" + # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" def titleize(word) - humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { $&.capitalize } + humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize } end - # Create 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. + # 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. # - # 'RawScaledScorer'.tableize # => "raw_scaled_scorers" - # 'egg_and_ham'.tableize # => "egg_and_hams" - # 'fancyCategory'.tableize # => "fancy_categories" + # tableize('RawScaledScorer') # => "raw_scaled_scorers" + # tableize('egg_and_ham') # => "egg_and_hams" + # tableize('fancyCategory') # => "fancy_categories" def tableize(class_name) pluralize(underscore(class_name)) end - # Create a class name from a plural table name like Rails does for table + # Creates a class name from a plural table name like Rails does for table # names to models. Note that this returns a string and not a Class (To - # convert to an actual class follow +classify+ with +constantize+). + # convert to an actual class follow +classify+ with #constantize). # - # 'egg_and_hams'.classify # => "EggAndHam" - # 'posts'.classify # => "Post" + # classify('egg_and_hams') # => "EggAndHam" + # classify('posts') # => "Post" # # Singular names are not handled correctly: # - # 'calculus'.classify # => "Calculu" + # classify('calculus') # => "Calculu" def classify(table_name) # strip out any leading schema name camelize(singularize(table_name.to_s.sub(/.*\./, ''))) @@ -183,19 +183,19 @@ module ActiveSupport # Replaces underscores with dashes in the string. # - # 'puni_puni'.dasherize # => "puni-puni" + # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) underscored_word.tr('_', '-') end # Removes the module part from the expression in the string. # - # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" - # 'Inflections'.demodulize # => "Inflections" - # '::Inflections'.demodulize # => "Inflections" - # ''.demodulize # => "" + # demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections" + # demodulize('Inflections') # => "Inflections" + # demodulize('::Inflections') # => "Inflections" + # demodulize('') # => "" # - # See also +deconstantize+. + # See also #deconstantize. def demodulize(path) path = path.to_s if i = path.rindex('::') @@ -207,13 +207,13 @@ module ActiveSupport # Removes the rightmost segment from the constant expression in the string. # - # 'Net::HTTP'.deconstantize # => "Net" - # '::Net::HTTP'.deconstantize # => "::Net" - # 'String'.deconstantize # => "" - # '::String'.deconstantize # => "" - # ''.deconstantize # => "" + # deconstantize('Net::HTTP') # => "Net" + # deconstantize('::Net::HTTP') # => "::Net" + # deconstantize('String') # => "" + # deconstantize('::String') # => "" + # deconstantize('') # => "" # - # See also +demodulize+. + # See also #demodulize. def deconstantize(path) path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename end @@ -222,17 +222,17 @@ module ActiveSupport # +separate_class_name_and_id_with_underscore+ sets whether # the method should put '_' between the name and 'id'. # - # 'Message'.foreign_key # => "message_id" - # 'Message'.foreign_key(false) # => "messageid" - # 'Admin::Post'.foreign_key # => "post_id" + # foreign_key('Message') # => "message_id" + # foreign_key('Message', false) # => "messageid" + # foreign_key('Admin::Post') # => "post_id" def foreign_key(class_name, separate_class_name_and_id_with_underscore = true) underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id") end # Tries to find a constant with the name specified in the argument string. # - # 'Module'.constantize # => Module - # 'Test::Unit'.constantize # => Test::Unit + # 'Module'.constantize # => Module + # 'Foo::Bar'.constantize # => Foo::Bar # # The name is assumed to be the one of a top-level constant, no matter # whether it starts with "::" or not. No lexical context is taken into @@ -280,8 +280,8 @@ module ActiveSupport # Tries to find a constant with the name specified in the argument string. # - # 'Module'.safe_constantize # => Module - # 'Test::Unit'.safe_constantize # => Test::Unit + # safe_constantize('Module') # => Module + # safe_constantize('Foo::Bar') # => Foo::Bar # # The name is assumed to be the one of a top-level constant, no matter # whether it starts with "::" or not. No lexical context is taken into @@ -290,16 +290,16 @@ module ActiveSupport # C = 'outside' # module M # C = 'inside' - # C # => 'inside' - # 'C'.safe_constantize # => 'outside', same as ::C + # C # => 'inside' + # safe_constantize('C') # => 'outside', same as ::C # end # # +nil+ is returned when the name is not in CamelCase or the constant (or # part of it) is unknown. # - # 'blargle'.safe_constantize # => nil - # 'UnknownModule'.safe_constantize # => nil - # 'UnknownModule::Foo::Bar'.safe_constantize # => nil + # safe_constantize('blargle') # => nil + # safe_constantize('UnknownModule') # => nil + # safe_constantize('UnknownModule::Foo::Bar') # => nil def safe_constantize(camel_cased_word) constantize(camel_cased_word) rescue NameError => e diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 1cde417fc5..edea142e82 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -67,17 +67,8 @@ module ActiveSupport # Replaces special characters in a string so that it may be used as part of # a 'pretty' URL. # - # class Person - # def to_param - # "#{id}-#{name.parameterize}" - # end - # end - # - # @person = Person.find(1) - # # => #<Person id: 1, name: "Donald E. Knuth"> - # - # <%= link_to(@person.name, person_path(@person)) %> - # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> + # parameterize("Donald E. Knuth") # => "donald-e-knuth" + # parameterize("^trés|Jolie-- ") # => "tres-jolie" def parameterize(string, sep = '-') # replace accented chars with their ascii equivalents parameterized_string = transliterate(string) @@ -92,6 +83,5 @@ module ActiveSupport end parameterized_string.downcase end - end end diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 35548f3f56..2932954f03 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -9,20 +9,14 @@ module ActiveSupport module JSON # matches YAML-formatted dates DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, options = {}) - if options.present? - raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ - "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ - "and has been removed." - end - + def decode(json) data = ::JSON.parse(json, quirks_mode: true) if ActiveSupport.parse_json_times diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 3c0cf9f137..45cf6fc1ef 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -86,8 +86,14 @@ module ActiveSupport #:nodoc: @wrapped_string.split(*args).map { |i| self.class.new(i) } end - # Works like like <tt>String#slice!</tt>, but returns an instance of + # Works like <tt>String#slice!</tt>, but returns an instance of # Chars, or nil if the string was not modified. + # + # string = 'Welcome' + # string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c"> + # string # => 'Welome' + # string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo"> + # string # => 'me' def slice!(*args) chars(@wrapped_string.slice!(*args)) end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 34439ee8be..258d9b34e1 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -94,9 +94,9 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # (defaults to 3). Keeps the number's precision if nil. + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -116,6 +116,7 @@ module ActiveSupport # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000% # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% # number_to_percentage(1000, locale: :fr) # => 1 000,000% + # number_to_percentage:(1000, precision: nil) # => 1000% # number_to_percentage('98a') # => 98a% # number_to_percentage(100, format: '%n %') # => 100 % def number_to_percentage(number, options = {}) @@ -161,9 +162,9 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # (defaults to 3). Keeps the number's precision if nil. + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +false+). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -182,6 +183,7 @@ module ActiveSupport # number_to_rounded(111.2345, significant: true) # => 111 # number_to_rounded(111.2345, precision: 1, significant: true) # => 100 # number_to_rounded(13, precision: 5, significant: true) # => 13.000 + # number_to_rounded(13, precision: nil) # => 13 # number_to_rounded(111.234, locale: :fr) # => 111,234 # # number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true) @@ -208,8 +210,8 @@ module ActiveSupport # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +true+) # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). @@ -258,8 +260,8 @@ module ActiveSupport # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number # (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # - # of significant_digits. If +false+, the # of fractional + # * <tt>:significant</tt> - If +true+, precision will be the number + # of significant_digits. If +false+, the number of fractional # digits (defaults to +true+) # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index fb5adb574a..7986eb50f0 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -13,7 +13,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit]) end private @@ -23,20 +23,20 @@ module ActiveSupport end def absolute_value(number) - number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '') + number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '') end def options @options ||= begin defaults = default_format_options.merge(i18n_opts) - # Override negative format if format options is given + # Override negative format if format options are given defaults[:negative_format] = "-#{opts[:format]}" if opts[:format] defaults.merge!(opts) end end def i18n_opts - # Set International negative format if not exists + # Set International negative format if it does not exist i18n = i18n_format_options i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format] i18n diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 6940beb318..5c6fe2df83 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -23,7 +23,7 @@ module ActiveSupport unit = determine_unit(units, exponent) rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 78d2c9ae6e..ac0d20b454 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 @@ -20,7 +20,7 @@ module ActiveSupport human_size = number / (base ** exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 1af294a03e..4c04d40c19 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -5,7 +5,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub(/%n/, rounded_number) + options[:format].gsub('%n'.freeze, rounded_number) end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index dcf9a567e8..981c562551 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -6,36 +6,39 @@ module ActiveSupport def convert precision = options.delete :precision - significant = options.delete :significant - case number - when Float, String - @number = BigDecimal(number.to_s) - when Rational - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = number.to_d - end - - if significant && precision > 0 - digits, rounded_number = digits_and_rounded_number(precision) - precision -= digits - precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = number.round(precision) - rounded_number = rounded_number.to_i if precision == 0 - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros - end + if precision + case number + when Float, String + @number = BigDecimal(number.to_s) + when Rational + @number = BigDecimal(number, digit_count(number.to_i) + precision) + else + @number = number.to_d + end - formatted_string = - if BigDecimal === rounded_number && rounded_number.finite? - s = rounded_number.to_s('F') + '0'*precision - a, b = s.split('.', 2) - a + '.' + b[0, precision] + if options.delete(:significant) && precision > 0 + digits, rounded_number = digits_and_rounded_number(precision) + precision -= digits + precision = 0 if precision < 0 # don't let it be negative else - "%00.#{precision}f" % rounded_number + rounded_number = number.round(precision) + rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite? + rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end + formatted_string = + if BigDecimal === rounded_number && rounded_number.finite? + s = rounded_number.to_s('F') + '0'*precision + a, b = s.split('.', 2) + a + '.' + b[0, precision] + else + "%00.#{precision}f" % rounded_number + end + else + formatted_string = number + end + delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) format_number(delimited_number) end diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index a33e2c58a9..bc0326473d 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -31,7 +31,13 @@ module ActiveSupport if name_string.chomp!('=') self[name_string] = args.first else - self[name] + bangs = name_string.chomp!('!') + + if bangs + fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank.")) + else + self[name_string] + end end end diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index b05c3ff126..c8e3a4bf53 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -1,8 +1,8 @@ # This is private interface. # # Rails components cherry pick from Active Support as needed, but there are a -# few features that are used for sure some way or another and it is not worth -# to put individual requires absolutely everywhere. Think blank? for example. +# few features that are used for sure in some way or another and it is not worth +# putting individual requires absolutely everywhere. Think blank? for example. # # This file is loaded by every Rails component except Active Support itself, # but it does not belong to the Rails public interface. It is internal to diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 6eba24b569..cd0fb51009 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,16 +13,14 @@ module ActiveSupport end end - initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app| - if app.config.active_support.key? :halt_callback_chains_on_return_false - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = \ - app.config.active_support.halt_callback_chains_on_return_false - end - end - # Sets the default value for Time.zone # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| + begin + TZInfo::DataSource.get + rescue TZInfo::DataSourceNotFound => e + raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install" + end require 'active_support/core_ext/time/zones' zone_default = Time.find_zone!(app.config.time_zone) diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 1a02acd5b1..fcf5553061 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -68,7 +68,7 @@ module ActiveSupport raise ArgumentError, "#{klass} is neither an Exception nor a String" end - # put the new handler at the end because the list is read in reverse + # Put the new handler at the end because the list is read in reverse. self.rescue_handlers += [[key, options[:with]]] end end @@ -100,7 +100,7 @@ module ActiveSupport # a string, otherwise a NameError will be raised by the interpreter # itself when rescue_from CONSTANT is executed. klass = self.class.const_get(klass_name) rescue nil - klass ||= klass_name.constantize rescue nil + klass ||= (klass_name.constantize rescue nil) klass === exception if klass end diff --git a/activesupport/lib/active_support/string_inquirer.rb b/activesupport/lib/active_support/string_inquirer.rb index 45271c9163..bc673150d0 100644 --- a/activesupport/lib/active_support/string_inquirer.rb +++ b/activesupport/lib/active_support/string_inquirer.rb @@ -1,7 +1,7 @@ module ActiveSupport # Wrapping a string in this class gives you a prettier way to test # for equality. The value returned by <tt>Rails.env</tt> is wrapped - # in a StringInquirer object so instead of calling this: + # in a StringInquirer object, so instead of calling this: # # Rails.env == 'production' # diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 98be78b41b..1cd4b807ad 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -5,24 +5,19 @@ module ActiveSupport # ActiveSupport::Notifications. The subscriber dispatches notifications to # a registered object based on its given namespace. # - # An example would be Active Record subscriber responsible for collecting + # An example would be an Active Record subscriber responsible for collecting # statistics about queries: # # module ActiveRecord # class StatsSubscriber < ActiveSupport::Subscriber + # attach_to :active_record + # # def sql(event) # Statsd.timing("sql.#{event.payload[:name]}", event.duration) # end # end # end # - # And it's finally registered as: - # - # ActiveRecord::StatsSubscriber.attach_to :active_record - # - # Since we need to know all instance methods before attaching the log - # subscriber, the line above should be called after your subscriber definition. - # # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the +sql+ method. @@ -66,7 +61,7 @@ module ActiveSupport pattern = "#{event}.#{namespace}" - # don't add multiple subscribers (eg. if methods are redefined) + # Don't add multiple subscribers (eg. if methods are redefined). return if subscriber.patterns.include?(pattern) subscriber.patterns << pattern @@ -96,7 +91,7 @@ module ActiveSupport event.end = finished event.payload.merge!(payload) - method = name.split('.').first + method = name.split('.'.freeze).first send(method, event) end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 9086a959aa..bcd7bf74c0 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -43,7 +43,7 @@ module ActiveSupport end def current_tags - # We use our object ID here to void conflicting with other instances + # We use our object ID here to avoid conflicting with other instances thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze Thread.current[thread_key] ||= [] end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index c7d6c62129..ae6f00b861 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -8,6 +8,8 @@ require 'active_support/testing/declarative' 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 @@ -35,17 +37,17 @@ module ActiveSupport # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. # Defaults to +:random+. def test_order - test_order = ActiveSupport.test_order + ActiveSupport.test_order ||= :random + end - if test_order.nil? - test_order = :random - self.test_order = test_order + def run(reporter, options = {}) + if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ } + options[:filter] = \ + Testing::CompositeFilter.new(self, options[:filter], options[:patterns]) end - test_order + super end - - alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end alias_method :method_name, :name @@ -55,6 +57,7 @@ module ActiveSupport include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation include ActiveSupport::Testing::TimeHelpers + include ActiveSupport::Testing::FileFixtures extend ActiveSupport::Testing::Declarative # test/unit backwards compatibility methods @@ -73,7 +76,7 @@ module ActiveSupport alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same - # Fails if the block raises an exception. + # Reveals the intention that the block should not raise any exception. # # assert_nothing_raised do # ... diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 8b649c193f..d87ce3474d 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -23,42 +23,42 @@ module ActiveSupport # result of what is evaluated in the yielded block. # # assert_difference 'Article.count' do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # An arbitrary expression is passed in and evaluated. # - # assert_difference 'assigns(:article).comments(:reload).size' do - # post :create, comment: {...} + # assert_difference 'Article.last.comments(:reload).size' do + # post :create, params: { comment: {...} } # end # # An arbitrary positive or negative difference can be specified. # The default is <tt>1</tt>. # # assert_difference 'Article.count', -1 do - # post :delete, id: ... + # post :delete, params: { id: ... } # end # # An array of expressions can also be passed in and evaluated. # # assert_difference [ 'Article.count', 'Post.count' ], 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # A lambda or a list of lambdas can be passed in and evaluated: # # assert_difference ->{ Article.count }, 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # assert_difference [->{ Article.count }, ->{ Post.count }], 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # An error message can be specified. # # assert_difference 'Article.count', -1, 'An Article should be destroyed' do - # post :delete, id: ... + # post :delete, params: { id: ... } # end def assert_difference(expression, difference = 1, message = nil, &block) expressions = Array(expression) @@ -81,13 +81,13 @@ module ActiveSupport # changed before and after invoking the passed in block. # # assert_no_difference 'Article.count' do - # post :create, article: invalid_attributes + # post :create, params: { article: invalid_attributes } # end # # An error message can be specified. # # assert_no_difference 'Article.count', 'An Article should not be created' do - # post :create, article: invalid_attributes + # post :create, params: { article: invalid_attributes } # end def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block 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/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb new file mode 100644 index 0000000000..4c6a0801b8 --- /dev/null +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -0,0 +1,34 @@ +module ActiveSupport + module Testing + # Adds simple access to sample files called file fixtures. + # File fixtures are normal files stored in + # <tt>ActiveSupport::TestCase.file_fixture_path</tt>. + # + # File fixtures are represented as +Pathname+ objects. + # This makes it easy to extract specific information: + # + # file_fixture("example.txt").read # get the file's content + # file_fixture("example.mp3").size # get the file size + module FileFixtures + extend ActiveSupport::Concern + + included do + class_attribute :file_fixture_path, instance_writer: false + end + + # Returns a +Pathname+ to the fixture file named +fixture_name+. + # + # Raises ArgumentError if +fixture_name+ can't be found. + def file_fixture(fixture_name) + path = Pathname.new(File.join(file_fixture_path, fixture_name)) + + if path.exist? + path + else + msg = "the directory '%s' does not contain a file named '%s'" + raise ArgumentError, msg % [file_fixture_path, fixture_name] + end + end + end + end +end diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 68bda35980..1de0a19998 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,5 +1,3 @@ -require 'rbconfig' - module ActiveSupport module Testing module Isolation @@ -12,7 +10,7 @@ module ActiveSupport end def self.forking_env? - !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) + !ENV["NO_FORK"] && Process.respond_to?(:fork) end @@class_setup_mutex = Mutex.new @@ -71,17 +69,17 @@ module ActiveSupport else Tempfile.open("isolation") do |tmpfile| env = { - ISOLATION_TEST: self.class.name, - ISOLATION_OUTPUT: tmpfile.path + 'ISOLATION_TEST' => self.class.name, + 'ISOLATION_OUTPUT' => tmpfile.path } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") orig_args = ORIG_ARGV.join(" ") test_opts = "-n#{self.class.name}##{self.name}" - command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + command = "#{Gem.ruby} #{load_paths} #{$0} '#{orig_args}' #{test_opts}" # IO.popen lets us pass env in a cross-platform way - child = IO.popen([env, command]) + child = IO.popen(env, command) begin Process.wait(child.pid) diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb new file mode 100644 index 0000000000..895192ad05 --- /dev/null +++ b/activesupport/lib/active_support/testing/stream.rb @@ -0,0 +1,42 @@ +module ActiveSupport + module Testing + module Stream #:nodoc: + private + + def silence_stream(stream) + old_stream = stream.dup + stream.reopen(IO::NULL) + stream.sync = true + yield + ensure + stream.reopen(old_stream) + old_stream.close + end + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + 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 8c63815660..fca0947c5b 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -39,15 +39,16 @@ module ActiveSupport end end - # Containing helpers that helps 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+ and +Date.today+. + # stubbing +Time.now+, +Date.today+, and +DateTime.now+. # - # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel 1.day - # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 - # Date.current # => Sun, 10 Nov 2013 + # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # Date.current # => Sun, 10 Nov 2013 + # DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500 # # This method also accepts a block, which will return the current time back to its original # state at the end of the block: @@ -61,13 +62,14 @@ module ActiveSupport travel_to Time.now + duration, &block end - # Changes current time to the given time by stubbing +Time.now+ and - # +Date.today+ to return the time or date passed into this method. + # Changes current time to the given time by stubbing +Time.now+, + # +Date.today+, and +DateTime.now+ to return the time or date passed into this method. # - # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) - # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 - # Date.current # => Wed, 24 Nov 2004 + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Date.current # => Wed, 24 Nov 2004 + # DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500 # # Dates are taken as their timestamp at the beginning of the day in the # application time zone. <tt>Time.current</tt> returns said timestamp, @@ -99,6 +101,7 @@ module ActiveSupport simple_stubs.stub_object(Time, :now, now) simple_stubs.stub_object(Date, :today, now.to_date) + simple_stubs.stub_object(DateTime, :now, now.to_datetime) if block_given? begin diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 8ddf233b3e..412c72d27c 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' @@ -169,12 +170,13 @@ module ActiveSupport end end - def encode_with(coder) - if coder.respond_to?(:represent_object) - coder.represent_object(nil, utc) - else - coder.represent_scalar(nil, utc.strftime("%Y-%m-%d %H:%M:%S.%9NZ")) - end + def init_with(coder) #:nodoc: + initialize(coder['utc'], coder['zone'], coder['time']) + end + + def encode_with(coder) #:nodoc: + coder.tag = '!ruby/object:ActiveSupport::TimeWithZone' + coder.map = { 'utc' => utc, 'zone' => time_zone, 'time' => time } end # Returns a string of the object's date and time in the format used by @@ -252,7 +254,7 @@ module ActiveSupport utc.hash end - # Adds an interval of time to the current object's time and return that + # Adds an interval of time to the current object's time and returns that # value as a new TimeWithZone object. # # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 17629eabb3..2699a064d7 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -111,9 +111,11 @@ module ActiveSupport "Jerusalem" => "Asia/Jerusalem", "Harare" => "Africa/Harare", "Pretoria" => "Africa/Johannesburg", + "Kaliningrad" => "Europe/Kaliningrad", "Moscow" => "Europe/Moscow", "St. Petersburg" => "Europe/Moscow", - "Volgograd" => "Europe/Moscow", + "Volgograd" => "Europe/Volgograd", + "Samara" => "Europe/Samara", "Kuwait" => "Asia/Kuwait", "Riyadh" => "Asia/Riyadh", "Nairobi" => "Africa/Nairobi", @@ -170,6 +172,7 @@ module ActiveSupport "Guam" => "Pacific/Guam", "Port Moresby" => "Pacific/Port_Moresby", "Magadan" => "Asia/Magadan", + "Srednekolymsk" => "Asia/Srednekolymsk", "Solomon Is." => "Pacific/Guadalcanal", "New Caledonia" => "Pacific/Noumea", "Fiji" => "Pacific/Fiji", @@ -221,13 +224,6 @@ module ActiveSupport @zones ||= zones_map.values.sort end - def zones_map - @zones_map ||= begin - MAPPING.each_key {|place| self[place]} # load all the zones - @lazy_zones_map - end - end - # Locate a specific time zone object. If the argument is a string, it # is interpreted to mean the name of the timezone to locate. If it is a # numeric value it is either the hour offset, or the second offset, of the @@ -254,6 +250,14 @@ module ActiveSupport def us_zones @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } end + + private + def zones_map + @zones_map ||= begin + MAPPING.each_key {|place| self[place]} # load all the zones + @lazy_zones_map + end + end end include Comparable @@ -344,24 +348,31 @@ module ActiveSupport # # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 def parse(str, now=now()) - parts = Date._parse(str, false) - return if parts.empty? - - time = Time.new( - parts.fetch(:year, now.year), - parts.fetch(:mon, now.month), - parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), - parts.fetch(:hour, 0), - parts.fetch(:min, 0), - parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), - parts.fetch(:offset, 0) - ) - - if parts[:offset] - TimeWithZone.new(time.utc, self) - else - TimeWithZone.new(nil, self, time) - end + parts_to_time(Date._parse(str, false), now) + end + + # Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone. + # + # Assumes that +str+ is a time in the time zone +self+, + # unless +format+ includes an explicit time zone. + # (This is the same behavior as +parse+.) + # In either case, the returned TimeWithZone has the timezone of +self+. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.strptime('1999-12-31 14:00:00', '%Y-%m-%d %H:%M:%S') # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # + # If upper components are missing from the string, they are supplied from + # TimeZone#now: + # + # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Time.zone.strptime('22:30:00', '%H:%M:%S') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 + def strptime(str, format, now=now()) + parts_to_time(DateTime._strptime(str, format), now) end # Returns an ActiveSupport::TimeWithZone instance representing the current @@ -417,7 +428,36 @@ module ActiveSupport tzinfo.periods_for_local(time) end + def init_with(coder) #:nodoc: + initialize(coder['name']) + end + + def encode_with(coder) #:nodoc: + coder.tag ="!ruby/object:#{self.class}" + coder.map = { 'name' => tzinfo.name } + end + private + def parts_to_time(parts, now) + return if parts.empty? + + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, 0) + ) + + if parts[:offset] + TimeWithZone.new(time.utc, self) + else + TimeWithZone.new(nil, self, time) + end + end + def time_now Time.now end diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 47a2824186..bb0ea9c582 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -75,5 +75,5 @@ module LibXML #:nodoc: end end -LibXML::XML::Document.send(:include, LibXML::Conversions::Document) -LibXML::XML::Node.send(:include, LibXML::Conversions::Node) +LibXML::XML::Document.include(LibXML::Conversions::Document) +LibXML::XML::Node.include(LibXML::Conversions::Node) diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 7398d4fa82..619cc7522d 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -77,7 +77,7 @@ module ActiveSupport end end - Nokogiri::XML::Document.send(:include, Conversions::Document) - Nokogiri::XML::Node.send(:include, Conversions::Node) + Nokogiri::XML::Document.include(Conversions::Document) + Nokogiri::XML::Node.include(Conversions::Node) end end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index f65ec962f9..9b88a2dcc1 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -37,9 +37,5 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end +require 'minitest/mock' require 'mocha/setup' # FIXME: stop using mocha - -# FIXME: we have tests that depend on run order, we should fix that and -# remove this method call. -require 'active_support/test_case' -ActiveSupport::TestCase.test_order = :sorted diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb new file mode 100644 index 0000000000..b25e5cca86 --- /dev/null +++ b/activesupport/test/array_inquirer_test.rb @@ -0,0 +1,36 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class ArrayInquirerTest < ActiveSupport::TestCase + def setup + @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet]) + end + + def test_individual + assert @array_inquirer.mobile? + assert @array_inquirer.tablet? + assert_not @array_inquirer.desktop? + end + + def test_any + assert @array_inquirer.any?(:mobile, :desktop) + assert @array_inquirer.any?(:watch, :tablet) + assert_not @array_inquirer.any?(:desktop, :watch) + end + + def test_any_with_block + assert @array_inquirer.any? { |v| v == :mobile } + assert_not @array_inquirer.any? { |v| v == :desktop } + end + + def test_respond_to + assert_respond_to @array_inquirer, :development? + end + + def test_inquiry + result = [:mobile, :tablet].inquiry + + assert_instance_of ActiveSupport::ArrayInquirer, result + assert_equal @array_inquirer, result + end +end diff --git a/activesupport/test/autoloading_fixtures/a/c/e/f.rb b/activesupport/test/autoloading_fixtures/a/c/e/f.rb deleted file mode 100644 index 57dba5a307..0000000000 --- a/activesupport/test/autoloading_fixtures/a/c/e/f.rb +++ /dev/null @@ -1,2 +0,0 @@ -class A::C::E::F -end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/a/c/em/f.rb b/activesupport/test/autoloading_fixtures/a/c/em/f.rb new file mode 100644 index 0000000000..8b28e19148 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/a/c/em/f.rb @@ -0,0 +1,2 @@ +class A::C::EM::F +end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/d.rb b/activesupport/test/autoloading_fixtures/d.rb new file mode 100644 index 0000000000..45c794d4ca --- /dev/null +++ b/activesupport/test/autoloading_fixtures/d.rb @@ -0,0 +1,2 @@ +class D +end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/e.rb b/activesupport/test/autoloading_fixtures/e.rb deleted file mode 100644 index 2f59e4fb75..0000000000 --- a/activesupport/test/autoloading_fixtures/e.rb +++ /dev/null @@ -1,2 +0,0 @@ -class E -end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/em.rb b/activesupport/test/autoloading_fixtures/em.rb new file mode 100644 index 0000000000..16a1838667 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/em.rb @@ -0,0 +1,2 @@ +class EM +end
\ No newline at end of file diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 98f3ea7a14..8e22d4ed2f 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -288,8 +288,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 @@ -387,54 +388,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 - 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" + def test_race_condition_protection_skipped_if_not_defined + @cache.write('foo', 'bar') + time = @cache.send(:read_entry, 'foo', {}).expires_at + + 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 + 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 + assert_equal "bar", @cache.read('foo') + end + Time.stub(:now, time + 91) do + assert_nil @cache.read('foo') + end + end + + def test_race_condition_protection + time = Time.now + @cache.write('foo', 'bar', :expires_in => 60) + Time.stub(:now, time + 61) do + result = @cache.fetch('foo', :race_condition_ttl => 10) do assert_equal 'bar', @cache.read('foo') - raise ArgumentError.new + "baz" end - rescue ArgumentError + assert_equal "baz", result end - assert_equal "bar", @cache.read('foo') - Time.stubs(:now).returns(time + 91) - assert_nil @cache.read('foo') end def test_crazy_key_characters @@ -624,37 +645,37 @@ module AutoloadingCacheBehavior include DependenciesTestHelpers def test_simple_autoloading with_autoloading_fixtures do - @cache.write('foo', E.new) + @cache.write('foo', EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, @cache.read('foo') + assert_kind_of EM, @cache.read('foo') end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear end def test_two_classes_autoloading with_autoloading_fixtures do - @cache.write('foo', [E.new, ClassFolder.new]) + @cache.write('foo', [EM.new, ClassFolder.new]) end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear with_autoloading_fixtures do loaded = @cache.read('foo') assert_kind_of Array, loaded assert_equal 2, loaded.size - assert_kind_of E, loaded[0] + assert_kind_of EM, loaded[0] assert_kind_of ClassFolder, loaded[1] end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear end end @@ -672,6 +693,7 @@ class FileStoreTest < ActiveSupport::TestCase def teardown FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT end def cache_dir @@ -691,6 +713,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + def test_long_keys @cache.write("a"*10000, 1) assert_equal 1, @cache.read("a"*10000) @@ -757,11 +784,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 @@ -1021,6 +1049,15 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase @cache.mute { @cache.fetch('foo') { 'bar' } } assert @buffer.string.blank? end + + def test_multi_read_loggin + @cache.write 'hello', 'goodbye' + @cache.write 'world', 'earth' + + @cache.read_multi('hello', 'world') + + assert_match "Caches multi read:\n- hello\n- world", @buffer.string + end end class CacheEntryTest < ActiveSupport::TestCase @@ -1029,9 +1066,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/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f6abef8cee..cda9732cae 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -73,8 +73,8 @@ module CallbacksTest class PersonSkipper < Person skip_callback :save, :before, :before_save_method, :if => :yes - skip_callback :save, :after, :before_save_method, :unless => :yes - skip_callback :save, :after, :before_save_method, :if => :no + skip_callback :save, :after, :after_save_method, :unless => :yes + skip_callback :save, :after, :after_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no skip_callback :save, :before, CallbackClass , :if => :yes def yes; true; end @@ -1021,7 +1021,7 @@ module CallbacksTest define_callbacks :foo n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end - def self.skip(thing); skip_callback :foo, :before, thing; end + def self.skip(*things); skip_callback :foo, :before, *things; end } end @@ -1070,11 +1070,11 @@ module CallbacksTest } end - def test_skip_lambda # removes nothing + def test_skip_lambda # raises error calls = [] callback = ->(o) { calls << o } klass = build_class(callback) - 10.times { klass.skip callback } + assert_raises(ArgumentError) { klass.skip callback } klass.new.run assert_equal 10, calls.length end @@ -1088,11 +1088,29 @@ module CallbacksTest assert_equal 0, calls.length end - def test_skip_eval # removes nothing + def test_skip_string # raises error calls = [] klass = build_class("bar") klass.class_eval { define_method(:bar) { calls << klass } } - klass.skip "bar" + assert_raises(ArgumentError) { klass.skip "bar" } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_undefined_callback # raises error + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + assert_raises(ArgumentError) { klass.skip :qux } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_without_raise # removes nothing + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip :qux, raise: false klass.new.run assert_equal 1, calls.length end diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 60bd8a06aa..8ea701cfb7 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -54,29 +54,54 @@ class ConcernTest < ActiveSupport::TestCase include Bar, Baz end + module Qux + module ClassMethods + end + end + def setup @klass = Class.new end def test_module_is_included_normally - @klass.send(:include, Baz) + @klass.include(Baz) assert_equal "baz", @klass.new.baz assert @klass.included_modules.include?(ConcernTest::Baz) end def test_class_methods_are_extended - @klass.send(:include, Baz) + @klass.include(Baz) assert_equal "baz", @klass.baz 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.send(:include, Baz) + @klass.include(Baz) assert_equal true, @klass.included_ran end def test_modules_dependencies_are_met - @klass.send(:include, Bar) + @klass.include(Bar) assert_equal "bar", @klass.new.bar assert_equal "bar+baz", @klass.new.baz assert_equal "bar's baz + baz", @klass.baz @@ -84,7 +109,7 @@ class ConcernTest < ActiveSupport::TestCase end def test_dependencies_with_multiple_modules - @klass.send(:include, Foo) + @klass.include(Foo) assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2] end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index ef847fc557..5d22ded2de 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -111,6 +111,14 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase end end + test 'the config_accessor method should not be publicly callable' do + assert_raises NoMethodError do + Class.new { + include ActiveSupport::Configurable + }.config_accessor :foo + end + end + def assert_method_defined(object, method) methods = object.public_methods.map(&:to_s) assert methods.include?(method.to_s), "Expected #{methods.inspect} to include #{method.to_s.inspect}" diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb index f14f64421d..3f1e0c4cb4 100644 --- a/activesupport/test/core_ext/array/access_test.rb +++ b/activesupport/test/core_ext/array/access_test.rb @@ -27,4 +27,8 @@ class AccessTest < ActiveSupport::TestCase assert_equal array[4], array.fifth assert_equal array[41], array.forty_two end + + def test_without + assert_equal [1, 2, 4], [1, 2, 3, 4, 5].without(3, 5) + end end diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 577b889410..507e13f968 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -60,6 +60,12 @@ class ToSentenceTest < ActiveSupport::TestCase assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" end + + def test_always_returns_string + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one')].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two'].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two', 'three'].to_sentence + end end class ToSTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb deleted file mode 100644 index 447b1d10ad..0000000000 --- a/activesupport/test/core_ext/class/delegating_attributes_test.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/class/delegating_attributes' - -module DelegatingFixtures - class Parent - end - - class Child < Parent - ActiveSupport::Deprecation.silence do - superclass_delegating_accessor :some_attribute - end - end - - class Mokopuna < Child - end - - class PercysMom - ActiveSupport::Deprecation.silence do - superclass_delegating_accessor :superpower - end - end - - class Percy < PercysMom - end -end - -class DelegatingAttributesTest < ActiveSupport::TestCase - include DelegatingFixtures - attr_reader :single_class - - def setup - @single_class = Class.new(Object) - end - - def test_simple_accessor_declaration - assert_deprecated do - single_class.superclass_delegating_accessor :both - end - - # Class should have accessor and mutator - # the instance should have an accessor only - assert_respond_to single_class, :both - assert_respond_to single_class, :both= - assert single_class.public_instance_methods.map(&:to_s).include?("both") - assert !single_class.public_instance_methods.map(&:to_s).include?("both=") - end - - def test_simple_accessor_declaration_with_instance_reader_false - _instance_methods = single_class.public_instance_methods - - assert_deprecated do - single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false - end - - assert_respond_to single_class, :no_instance_reader - assert_respond_to single_class, :no_instance_reader= - assert !_instance_methods.include?(:no_instance_reader) - assert !_instance_methods.include?(:no_instance_reader?) - assert !_instance_methods.include?(:_no_instance_reader) - end - - def test_working_with_simple_attributes - assert_deprecated do - single_class.superclass_delegating_accessor :both - end - - single_class.both = "HMMM" - - assert_equal "HMMM", single_class.both - assert_equal true, single_class.both? - - assert_equal "HMMM", single_class.new.both - assert_equal true, single_class.new.both? - - single_class.both = false - assert_equal false, single_class.both? - end - - def test_child_class_delegates_to_parent_but_can_be_overridden - parent = Class.new - - assert_deprecated do - parent.superclass_delegating_accessor :both - end - - child = Class.new(parent) - parent.both = "1" - assert_equal "1", child.both - - child.both = "2" - assert_equal "1", parent.both - assert_equal "2", child.both - - parent.both = "3" - assert_equal "3", parent.both - assert_equal "2", child.both - end - - def test_delegation_stops_at_the_right_level - assert_nil Percy.superpower - assert_nil PercysMom.superpower - - PercysMom.superpower = :heatvision - assert_equal :heatvision, Percy.superpower - end - - def test_delegation_stops_for_nil - Mokopuna.some_attribute = nil - Child.some_attribute="1" - - assert_equal "1", Child.some_attribute - assert_nil Mokopuna.some_attribute - ensure - Child.some_attribute=nil - end - - def test_deprecation_warning - assert_deprecated(/superclass_delegating_accessor is deprecated/) do - single_class.superclass_delegating_accessor :test_attribute - end - end -end diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index b4ef5a0597..784547bdf8 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -6,11 +6,21 @@ module DateAndTimeBehavior assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).yesterday.yesterday end + def test_prev_day + assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).prev_day + assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).prev_day.prev_day + end + def test_tomorrow assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).tomorrow assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).tomorrow.tomorrow end + def test_next_day + assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).next_day + assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).next_day.next_day + end + def test_days_ago assert_equal date_time_init(2005,6,4,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(1) assert_equal date_time_init(2005,5,31,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(5) @@ -115,6 +125,28 @@ module DateAndTimeBehavior end end + def test_next_week_at_same_time + assert_equal date_time_init(2005,2,28,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:monday, same_time: true) + assert_equal date_time_init(2005,3,4,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:friday, same_time: true) + assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:monday, same_time: true) + assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday, same_time: true) + end + + def test_next_weekday_on_wednesday + assert_equal date_time_init(2015,1,8,0,0,0), date_time_init(2015,1,7,0,0,0).next_weekday + assert_equal date_time_init(2015,1,8,15,15,10), date_time_init(2015,1,7,15,15,10).next_weekday + end + + def test_next_weekday_on_friday + assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,2,0,0,0).next_weekday + assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,2,15,15,10).next_weekday + end + + def test_next_weekday_on_saturday + assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,3,0,0,0).next_weekday + assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,3,15,15,10).next_weekday + end + def test_next_month_on_31st assert_equal date_time_init(2005,9,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_month end @@ -144,6 +176,29 @@ module DateAndTimeBehavior end end + def test_prev_week_at_same_time + assert_equal date_time_init(2005,2,21,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:monday, same_time: true) + assert_equal date_time_init(2005,2,22,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday, same_time: true) + assert_equal date_time_init(2005,2,25,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:friday, same_time: true) + assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week(:monday, same_time: true) + assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday, same_time: true) + end + + def test_prev_weekday_on_wednesday + assert_equal date_time_init(2015,1,6,0,0,0), date_time_init(2015,1,7,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,6,15,15,10), date_time_init(2015,1,7,15,15,10).prev_weekday + end + + def test_prev_weekday_on_monday + assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,5,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,5,15,15,10).prev_weekday + end + + def test_prev_weekday_on_sunday + assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,4,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,4,15,15,10).prev_weekday + end + def test_prev_month_on_31st assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,3,31,10,10,10).prev_month end @@ -231,6 +286,21 @@ module DateAndTimeBehavior end end + def test_on_weekend_on_saturday + assert date_time_init(2015,1,3,0,0,0).on_weekend? + assert date_time_init(2015,1,3,15,15,10).on_weekend? + end + + def test_on_weekend_on_sunday + assert date_time_init(2015,1,4,0,0,0).on_weekend? + assert date_time_init(2015,1,4,15,15,10).on_weekend? + end + + def test_on_weekend_on_monday + assert_not date_time_init(2015,1,5,0,0,0).on_weekend? + assert_not date_time_init(2015,1,5,15,15,10).on_weekend? + end + def with_bw_default(bw = :monday) old_bw = Date.beginning_of_week Date.beginning_of_week = bw 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 fd23b32029..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 diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 2b893c7cd0..9e97acaffb 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -70,6 +70,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal '14 days', 1.fortnight.inspect end + def test_inspect_locale + current_locale = I18n.default_locale + I18n.default_locale = :de + I18n.backend.store_translations(:de, { support: { array: { last_word_connector: ' und ' } } }) + assert_equal '10 years, 1 month und 1 day', (10.years + 1.month + 1.day).inspect + ensure + I18n.default_locale = current_locale + end + def test_minus_with_duration_does_not_break_subtraction_of_date_from_date assert_nothing_raised { Date.today - Date.today } end @@ -131,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 346dc3d208..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 @@ -103,4 +105,23 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal true, GenericEnumerable.new([ 1 ]).exclude?(2) assert_equal false, GenericEnumerable.new([ 1 ]).exclude?(1) end + + def test_without + assert_equal [1, 2, 4], GenericEnumerable.new((1..5).to_a).without(3, 5) + assert_equal [1, 2, 4], (1..5).to_a.without(3, 5) + assert_equal [1, 2, 4], (1..5).to_set.without(3, 5) + assert_equal({foo: 1, baz: 3}, {foo: 1, bar: 2, baz: 3}.without(:bar)) + end + + 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/file_test.rb b/activesupport/test/core_ext/file_test.rb index 2c04e9687c..cde0132b97 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -57,6 +57,16 @@ class AtomicWriteTest < ActiveSupport::TestCase File.unlink(file_name) rescue nil end + def test_atomic_write_returns_result_from_yielded_block + block_return_value = File.atomic_write(file_name, Dir.pwd) do |file| + "Hello world!" + end + + assert_equal "Hello world!", block_return_value + ensure + File.unlink(file_name) rescue nil + end + private def file_name "atomic.file" diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 3d2f50ce49..e10bee5e00 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -1549,6 +1549,14 @@ class HashToXmlTest < ActiveSupport::TestCase assert_not_same hash_wia, hash_wia.with_indifferent_access end + + def test_allows_setting_frozen_array_values_with_indifferent_access + value = [1, 2, 3].freeze + hash = HashWithIndifferentAccess.new + hash[:key] = value + assert_equal hash[:key], value + end + def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access hash = Hash.new(3) hash_wia = hash.with_indifferent_access diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index 8f3f710dfd..825df439a5 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -8,28 +8,28 @@ class MarshalTest < ActiveSupport::TestCase def teardown ActiveSupport::Dependencies.clear - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) end test "that Marshal#load still works" do sanity_data = ["test", [1, 2, 3], {a: [1, 2, 3]}, ActiveSupport::TestCase] sanity_data.each do |obj| dumped = Marshal.dump(obj) - assert_equal Marshal.load_without_autoloading(dumped), Marshal.load(dumped) + assert_equal Marshal.method(:load).super_method.call(dumped), Marshal.load(dumped) end end test "that a missing class is autoloaded from string" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump(E.new) + dumped = Marshal.dump(EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, Marshal.load(dumped) + assert_kind_of EM, Marshal.load(dumped) end end @@ -50,16 +50,16 @@ class MarshalTest < ActiveSupport::TestCase test "that more than one missing class is autoloaded" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump([E.new, ClassFolder.new]) + dumped = Marshal.dump([EM.new, ClassFolder.new]) end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear with_autoloading_fixtures do loaded = Marshal.load(dumped) assert_equal 2, loaded.size - assert_kind_of E, loaded[0] + assert_kind_of EM, loaded[0] assert_kind_of ClassFolder, loaded[1] end end @@ -67,10 +67,10 @@ class MarshalTest < ActiveSupport::TestCase test "that a real missing class is causing an exception" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump(E.new) + dumped = Marshal.dump(EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear assert_raise(NameError) do @@ -84,10 +84,10 @@ class MarshalTest < ActiveSupport::TestCase end with_autoloading_fixtures do - dumped = Marshal.dump([E.new, SomeClass.new]) + dumped = Marshal.dump([EM.new, SomeClass.new]) end - remove_constants(:E) + remove_constants(:EM) self.class.send(:remove_const, :SomeClass) ActiveSupport::Dependencies.clear @@ -96,8 +96,8 @@ class MarshalTest < ActiveSupport::TestCase Marshal.load(dumped) end - assert_nothing_raised("E failed to load while we expect only SomeClass to fail loading") do - E.new + assert_nothing_raised("EM failed to load while we expect only SomeClass to fail loading") do + EM.new end assert_raise(NameError, "We expected SomeClass to not be loaded but it is!") do @@ -109,16 +109,16 @@ class MarshalTest < ActiveSupport::TestCase test "loading classes from files trigger autoloading" do Tempfile.open("object_serializer_test") do |f| with_autoloading_fixtures do - Marshal.dump(E.new, f) + Marshal.dump(EM.new, f) end f.rewind - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, Marshal.load(f) + assert_kind_of EM, Marshal.load(f) end end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 3c49c4d14f..bdfbadcf1d 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -78,7 +78,7 @@ Product = Struct.new(:name) do def type @type ||= begin - :thing_without_same_method_name_as_delegated.name + nil.type_name end end end @@ -358,148 +358,178 @@ class MethodAliasingTest < ActiveSupport::TestCase Object.instance_eval { remove_const :FooClassWithBarMethod } end - def test_alias_method_chain - assert @instance.respond_to?(:bar) - feature_aliases = [:bar_with_baz, :bar_without_baz] + def test_alias_method_chain_deprecated + assert_deprecated(/alias_method_chain/) do + Module.new do + def base + end + + def base_with_deprecated + end - feature_aliases.each do |method| - assert !@instance.respond_to?(method) + alias_method_chain :base, :deprecated + end end + end - assert_equal 'bar', @instance.bar + def test_alias_method_chain + assert_deprecated(/alias_method_chain/) do + assert @instance.respond_to?(:bar) + feature_aliases = [:bar_with_baz, :bar_without_baz] - FooClassWithBarMethod.class_eval { include BarMethodAliaser } + feature_aliases.each do |method| + assert !@instance.respond_to?(method) + end - feature_aliases.each do |method| - assert_respond_to @instance, method - end + assert_equal 'bar', @instance.bar + + FooClassWithBarMethod.class_eval { include BarMethodAliaser } + + feature_aliases.each do |method| + assert_respond_to @instance, method + end - assert_equal 'bar_with_baz', @instance.bar - assert_equal 'bar', @instance.bar_without_baz + assert_equal 'bar_with_baz', @instance.bar + assert_equal 'bar', @instance.bar_without_baz + end end def test_alias_method_chain_with_punctuation_method - FooClassWithBarMethod.class_eval do - def quux!; 'quux' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux' end + end - assert !@instance.respond_to?(:quux_with_baz!) - FooClassWithBarMethod.class_eval do - include BarMethodAliaser - alias_method_chain :quux!, :baz - end - assert_respond_to @instance, :quux_with_baz! + assert !@instance.respond_to?(:quux_with_baz!) + FooClassWithBarMethod.class_eval do + include BarMethodAliaser + alias_method_chain :quux!, :baz + end + assert_respond_to @instance, :quux_with_baz! - assert_equal 'quux_with_baz', @instance.quux! - assert_equal 'quux', @instance.quux_without_baz! + assert_equal 'quux_with_baz', @instance.quux! + assert_equal 'quux', @instance.quux_without_baz! + end end def test_alias_method_chain_with_same_names_between_predicates_and_bang_methods - FooClassWithBarMethod.class_eval do - def quux!; 'quux!' end - def quux?; true end - def quux=(v); 'quux=' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux!' end + def quux?; true end + def quux=(v); 'quux=' end + end - assert !@instance.respond_to?(:quux_with_baz!) - assert !@instance.respond_to?(:quux_with_baz?) - assert !@instance.respond_to?(:quux_with_baz=) + assert !@instance.respond_to?(:quux_with_baz!) + assert !@instance.respond_to?(:quux_with_baz?) + assert !@instance.respond_to?(:quux_with_baz=) - FooClassWithBarMethod.class_eval { include BarMethodAliaser } - assert_respond_to @instance, :quux_with_baz! - assert_respond_to @instance, :quux_with_baz? - assert_respond_to @instance, :quux_with_baz= + FooClassWithBarMethod.class_eval { include BarMethodAliaser } + assert_respond_to @instance, :quux_with_baz! + assert_respond_to @instance, :quux_with_baz? + assert_respond_to @instance, :quux_with_baz= - FooClassWithBarMethod.alias_method_chain :quux!, :baz - assert_equal 'quux!_with_baz', @instance.quux! - assert_equal 'quux!', @instance.quux_without_baz! + FooClassWithBarMethod.alias_method_chain :quux!, :baz + assert_equal 'quux!_with_baz', @instance.quux! + assert_equal 'quux!', @instance.quux_without_baz! - FooClassWithBarMethod.alias_method_chain :quux?, :baz - assert_equal false, @instance.quux? - assert_equal true, @instance.quux_without_baz? + FooClassWithBarMethod.alias_method_chain :quux?, :baz + assert_equal false, @instance.quux? + assert_equal true, @instance.quux_without_baz? - FooClassWithBarMethod.alias_method_chain :quux=, :baz - assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) - assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + FooClassWithBarMethod.alias_method_chain :quux=, :baz + assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) + assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + end end def test_alias_method_chain_with_feature_punctuation - FooClassWithBarMethod.class_eval do - def quux; 'quux' end - def quux?; 'quux?' end - include BarMethodAliaser - alias_method_chain :quux, :baz! - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux; 'quux' end + def quux?; 'quux?' end + include BarMethodAliaser + alias_method_chain :quux, :baz! + end - assert_nothing_raised do - assert_equal 'quux_with_baz', @instance.quux_with_baz! - end + assert_nothing_raised do + assert_equal 'quux_with_baz', @instance.quux_with_baz! + end - assert_raise(NameError) do - FooClassWithBarMethod.alias_method_chain :quux?, :baz! + assert_raise(NameError) do + FooClassWithBarMethod.alias_method_chain :quux?, :baz! + end end end def test_alias_method_chain_yields_target_and_punctuation - args = nil + assert_deprecated(/alias_method_chain/) do + args = nil - FooClassWithBarMethod.class_eval do - def quux?; end - include BarMethods + FooClassWithBarMethod.class_eval do + def quux?; end + include BarMethods - FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| - args = [target, punctuation] + FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| + args = [target, punctuation] + end end - end - assert_not_nil args - assert_equal 'quux', args[0] - assert_equal '?', args[1] + assert_not_nil args + assert_equal 'quux', args[0] + assert_equal '?', args[1] + end end def test_alias_method_chain_preserves_private_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - private :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + private :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.private_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.private_method_defined?(:duck) + end end def test_alias_method_chain_preserves_protected_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - protected :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + protected :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.protected_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.protected_method_defined?(:duck) + end end def test_alias_method_chain_preserves_public_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - public :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + public :duck + alias_method_chain :duck, :orange + end - assert_equal 'duck_with_orange', @instance.duck - assert FooClassWithBarMethod.public_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.duck + assert FooClassWithBarMethod.public_method_defined?(:duck) + end end def test_delegate_with_case diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index b82448458d..2d8796179e 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -389,4 +389,88 @@ class NumericExtFormattingTest < ActiveSupport::TestCase def test_in_milliseconds assert_equal 10_000, 10.seconds.in_milliseconds end + + # TODO: Remove positive and negative tests when we drop support to ruby < 2.3 + b = 2**64 + b *= b until Bignum === b + + T_ZERO = b.coerce(0).first + T_ONE = b.coerce(1).first + T_MONE = b.coerce(-1).first + + def test_positive + assert_predicate(1, :positive?) + assert_not_predicate(0, :positive?) + assert_not_predicate(-1, :positive?) + assert_predicate(+1.0, :positive?) + assert_not_predicate(+0.0, :positive?) + assert_not_predicate(-0.0, :positive?) + assert_not_predicate(-1.0, :positive?) + assert_predicate(+(0.0.next_float), :positive?) + assert_not_predicate(-(0.0.next_float), :positive?) + assert_predicate(Float::INFINITY, :positive?) + assert_not_predicate(-Float::INFINITY, :positive?) + assert_not_predicate(Float::NAN, :positive?) + + a = Class.new(Numeric) do + def >(x); true; end + end.new + assert_predicate(a, :positive?) + + a = Class.new(Numeric) do + def >(x); false; end + end.new + assert_not_predicate(a, :positive?) + + assert_predicate(1/2r, :positive?) + assert_not_predicate(-1/2r, :positive?) + + assert_predicate(T_ONE, :positive?) + assert_not_predicate(T_MONE, :positive?) + assert_not_predicate(T_ZERO, :positive?) + + e = assert_raises(NoMethodError) do + Complex(1).positive? + end + + assert_match(/positive\?/, e.message) + end + + def test_negative + assert_predicate(-1, :negative?) + assert_not_predicate(0, :negative?) + assert_not_predicate(1, :negative?) + assert_predicate(-1.0, :negative?) + assert_not_predicate(-0.0, :negative?) + assert_not_predicate(+0.0, :negative?) + assert_not_predicate(+1.0, :negative?) + assert_predicate(-(0.0.next_float), :negative?) + assert_not_predicate(+(0.0.next_float), :negative?) + assert_predicate(-Float::INFINITY, :negative?) + assert_not_predicate(Float::INFINITY, :negative?) + assert_not_predicate(Float::NAN, :negative?) + + a = Class.new(Numeric) do + def <(x); true; end + end.new + assert_predicate(a, :negative?) + + a = Class.new(Numeric) do + def <(x); false; end + end.new + assert_not_predicate(a, :negative?) + + assert_predicate(-1/2r, :negative?) + assert_not_predicate(1/2r, :negative?) + + assert_not_predicate(T_ONE, :negative?) + assert_predicate(T_MONE, :negative?) + assert_not_predicate(T_ZERO, :negative?) + + e = assert_raises(NoMethodError) do + Complex(1).negative? + end + + assert_match(/negative\?/, e.message) + end end diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 246bc7fa61..8a5e385dd7 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_support/core_ext/object/blank' diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 91d558dbb5..791b5e7172 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -50,4 +50,10 @@ class DeepDupTest < ActiveSupport::TestCase assert dup.instance_variable_defined?(:@a) end + def test_deep_dup_with_hash_class_key + hash = { Fixnum => 1 } + dup = hash.deep_dup + assert_equal 1, dup.keys.length + end + end diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index d37f4bd0d8..042f5cfb34 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -9,6 +9,9 @@ class DuplicableTest < ActiveSupport::TestCase ALLOW_DUP << BigDecimal.new('4.56') def test_duplicable + rubinius_skip "* Method#dup is allowed at the moment on Rubinius\n" \ + "* https://github.com/rubinius/rubinius/issues/3089" + RAISE_DUP.each do |v| assert !v.duplicable? assert_raises(TypeError, v.class.name) { v.dup } diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index 89438675c1..5ea0f0eca6 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -77,9 +77,9 @@ class ObjectTryTest < ActiveSupport::TestCase klass = Class.new do private - def private_method - 'private method' - end + def private_method + 'private method' + end end assert_raise(NoMethodError) { klass.new.try!(:private_method) } @@ -89,11 +89,75 @@ class ObjectTryTest < ActiveSupport::TestCase klass = Class.new do private - def private_method - 'private method' - end + def private_method + 'private method' + end end assert_nil klass.new.try(:private_method) end + + class Decorator < SimpleDelegator + def delegator_method + 'delegator method' + end + + def reverse + 'overridden reverse' + end + + private + + def private_delegator_method + 'private delegator method' + end + end + + def test_try_with_method_on_delegator + assert_equal 'delegator method', Decorator.new(@string).try(:delegator_method) + end + + def test_try_with_method_on_delegator_target + assert_equal 5, Decorator.new(@string).size + end + + def test_try_with_overriden_method_on_delegator + assert_equal 'overridden reverse', Decorator.new(@string).reverse + end + + def test_try_with_private_method_on_delegator + assert_nil Decorator.new(@string).try(:private_delegator_method) + end + + def test_try_with_private_method_on_delegator_bang + assert_raise(NoMethodError) do + Decorator.new(@string).try!(:private_delegator_method) + end + end + + def test_try_with_private_method_on_delegator_target + klass = Class.new do + private + + def private_method + 'private method' + end + end + + assert_nil Decorator.new(klass.new).try(:private_method) + end + + def test_try_with_private_method_on_delegator_target_bang + klass = Class.new do + private + + def private_method + 'private method' + end + end + + assert_raise(NoMethodError) do + Decorator.new(klass.new).try!(:private_method) + end + end end diff --git a/activesupport/test/core_ext/secure_random_test.rb b/activesupport/test/core_ext/secure_random_test.rb new file mode 100644 index 0000000000..dfacb7fe9f --- /dev/null +++ b/activesupport/test/core_ext/secure_random_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' +require 'active_support/core_ext/securerandom' + +class SecureRandomTest < ActiveSupport::TestCase + def test_base58 + s1 = SecureRandom.base58 + s2 = SecureRandom.base58 + + assert_not_equal s1, s2 + assert_equal 16, s1.length + end + + def test_base58_with_length + s1 = SecureRandom.base58(24) + s2 = SecureRandom.base58(24) + + assert_not_equal s1, s2 + assert_equal 24, s1.length + end +end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index cdc695f036..3a5d6df06d 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'date' require 'abstract_unit' require 'inflector_test_cases' @@ -250,6 +249,15 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>') end + def test_truncate_words_with_complex_string + Timeout.timeout(10) do + complex_string = "aa aa aaa aa aaa aaa aaa aa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaaa aaaaa aaaaa aaaaaa aa aa aa aaa aa aaa aa aa aa aa a aaa aaa \n a aaa <<s" + assert_equal complex_string.truncate_words(80), complex_string + end + rescue Timeout::Error + assert false + end + def test_truncate_multibyte assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10) @@ -427,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 c8364ecdff..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 diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 92c233d567..5b48bf328c 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'active_support/core_ext/string/strip' class TimeWithZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -123,11 +124,53 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_to_yaml - assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml) + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, @twz.to_yaml) end def test_ruby_to_yaml - assert_match(/---\s*\n:twz: 2000-01-01 00:00:00(\.0+)?\s*Z\n/, {:twz => @twz}.to_yaml) + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, { 'twz' => @twz }.to_yaml) + end + + def test_yaml_load + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(@twz, YAML.load(yaml)) + end + + def test_ruby_yaml_load + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal({ 'twz' => @twz }, YAML.load(yaml)) end def test_httpdate @@ -162,45 +205,50 @@ 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? @@ -990,19 +1038,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/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 43a5997ddd..1694fe7e72 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'uri' require 'active_support/core_ext/uri' diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 702e26859a..6dce7560dd 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -160,7 +160,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_ensures_the_expected_constant_is_defined with_autoloading_fixtures do e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -178,7 +178,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 1, TypO e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -187,7 +187,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Module, A assert_kind_of Class, A::B assert_kind_of Class, A::C::D - assert_kind_of Class, A::C::E::F + assert_kind_of Class, A::C::EM::F end end @@ -552,24 +552,24 @@ class DependenciesTest < ActiveSupport::TestCase def test_const_missing_in_anonymous_modules_loads_top_level_constants with_autoloading_fixtures do # class_eval STRING pushes the class to the nesting of the eval'ed code. - klass = Class.new.class_eval "E" - assert_equal E, klass + klass = Class.new.class_eval "EM" + assert_equal EM, klass end ensure - remove_constants(:E) + remove_constants(:EM) end def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object with_autoloading_fixtures do - require_dependency 'e' + require_dependency 'em' mod = Module.new - e = assert_raise(NameError) { mod::E } - assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message - assert_equal :E, e.name + e = assert_raise(NameError) { mod::EM } + assert_equal 'EM cannot be autoloaded from an anonymous class or module', e.message + assert_equal :EM, e.name end ensure - remove_constants(:E) + remove_constants(:EM) end def test_removal_from_tree_should_be_detected @@ -664,19 +664,19 @@ class DependenciesTest < ActiveSupport::TestCase def test_preexisting_constants_are_not_marked_as_autoloaded with_autoloading_fixtures do - require_dependency 'e' - assert ActiveSupport::Dependencies.autoloaded?(:E) + require_dependency 'em' + assert ActiveSupport::Dependencies.autoloaded?(:EM) ActiveSupport::Dependencies.clear end - Object.const_set :E, Class.new + Object.const_set :EM, Class.new with_autoloading_fixtures do - require_dependency 'e' - assert ! ActiveSupport::Dependencies.autoloaded?(:E), "E shouldn't be marked autoloaded!" + require_dependency 'em' + assert ! ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!" ActiveSupport::Dependencies.clear end ensure - remove_constants(:E) + remove_constants(:EM) end def test_constants_in_capitalized_nesting_marked_as_autoloaded diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 7aff56cbad..20bd8ee5dd 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/testing/stream' class Deprecatee def initialize @@ -36,6 +37,8 @@ end class DeprecationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Stream + def setup # Track the last warning. @old_behavior = ActiveSupport::Deprecation.behavior @@ -356,20 +359,4 @@ class DeprecationTest < ActiveSupport::TestCase deprecator end - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end end diff --git a/activesupport/test/file_fixtures/sample.txt b/activesupport/test/file_fixtures/sample.txt new file mode 100644 index 0000000000..0fa80e7383 --- /dev/null +++ b/activesupport/test/file_fixtures/sample.txt @@ -0,0 +1 @@ +sample file fixture diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index 843994147b..1facd691fa 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -7,4 +7,5 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase hash.reverse_merge! key: :new_value assert_equal :old_value, hash[:key] end -end
\ No newline at end of file + +end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 3770f00fe3..18a8b92eb9 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module InflectorTestCases SingularToPlural = { diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 80bf255080..f2fc456f4b 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_support/json' require 'active_support/time' diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 5c5045da1e..2f269a66f0 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'securerandom' require 'abstract_unit' require 'active_support/core_ext/string/inflections' @@ -131,6 +130,8 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_process_status + rubinius_skip "https://github.com/rubinius/rubinius/issues/3334" + # There doesn't seem to be a good way to get a handle on a Process::Status object without actually # creating a child process, hence this to populate $? system("not_a_real_program_#{SecureRandom.hex}") diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 94748dd991..e1c4b705f8 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'multibyte_test_helpers' require 'active_support/core_ext/string/multibyte' diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index f7bd21c8ab..d8704716e7 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'multibyte_test_helpers' diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb index d8ffd7ca9c..11f5374017 100644 --- a/activesupport/test/multibyte_proxy_test.rb +++ b/activesupport/test/multibyte_proxy_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 90af2dadd4..2e4b5cc873 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module MultibyteTestHelpers UNICODE_STRING = 'こにちわ'.freeze diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb index bec65daf50..52ae266f01 100644 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ b/activesupport/test/multibyte_unicode_database_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 50d84a9470..83efbffdfb 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -83,6 +83,14 @@ module ActiveSupport assert_equal("98a%", number_helper.number_to_percentage("98a")) assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN)) assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY)) + assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN, precision: 0)) + assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY, precision: 0)) + assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN, precision: 1)) + assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY, precision: 1)) + assert_equal("1000%", number_helper.number_to_percentage(1000, precision: nil)) + assert_equal("1000%", number_helper.number_to_percentage(1000, precision: nil)) + assert_equal("1000.1%", number_helper.number_to_percentage(1000.1, precision: nil)) + assert_equal("-0.13 %", number_helper.number_to_percentage("-0.13", precision: nil, format: "%n %")) end end diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index fdc745b23b..18767a3536 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -85,4 +85,19 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert_equal 42, a.method(:blah=).call(42) assert_equal 42, a.method(:blah).call end + + def test_raises_with_bang + a = ActiveSupport::OrderedOptions.new + a[:foo] = :bar + assert a.respond_to?(:foo!) + + assert_nothing_raised { a.foo! } + assert_equal a.foo, a.foo! + + assert_raises(KeyError) do + a.foo = nil + a.foo! + end + assert_raises(KeyError) { a.non_existing_key! } + end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 4532152996..18fb6d2fbf 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -61,6 +61,13 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal({'str' => str}, YAML.load(yaml)) end + test "Should work with primitive-like-strings in to_yaml conversion" do + assert_equal 'true', YAML.load(ActiveSupport::SafeBuffer.new('true').to_yaml) + assert_equal 'false', YAML.load(ActiveSupport::SafeBuffer.new('false').to_yaml) + assert_equal '1', YAML.load(ActiveSupport::SafeBuffer.new('1').to_yaml) + assert_equal '1.1', YAML.load(ActiveSupport::SafeBuffer.new('1.1').to_yaml) + end + test "Should work with underscore" do str = "MyTest".html_safe.underscore assert_equal "my_test", str diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 151b623171..9e6d1a91d0 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -205,15 +205,4 @@ class TestOrderTest < ActiveSupport::TestCase assert_equal :random, self.class.test_order assert_equal :random, Class.new(ActiveSupport::TestCase).test_order end - - def test_i_suck_and_my_tests_are_order_dependent! - ActiveSupport::TestCase.test_order = :random - - klass = Class.new(ActiveSupport::TestCase) do - i_suck_and_my_tests_are_order_dependent! - end - - assert_equal :alpha, klass.test_order - assert_equal :random, ActiveSupport::TestCase.test_order - end end diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb new file mode 100644 index 0000000000..91b8a9071c --- /dev/null +++ b/activesupport/test/testing/file_fixtures_test.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' + +class FileFixturesTest < ActiveSupport::TestCase + self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__) + + test "#file_fixture returns Pathname to file fixture" do + path = file_fixture("sample.txt") + assert_kind_of Pathname, path + assert_match %r{activesupport/test/file_fixtures/sample.txt$}, path.to_s + end + + test "raises an exception when the fixture file does not exist" do + e = assert_raises(ArgumentError) do + file_fixture("nope") + end + assert_match(/^the directory '[^']+test\/file_fixtures' does not contain a file named 'nope'$/, e.message) + end +end + +class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase + self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__)) + + test "#file_fixture_path returns Pathname to file fixture" do + path = file_fixture("sample.txt") + assert_kind_of Pathname, path + assert_match %r{activesupport/test/file_fixtures/sample.txt$}, path.to_s + end +end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 065539671d..59c3e52c2f 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -1,72 +1,90 @@ require 'abstract_unit' -require 'active_support/core_ext/date' +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 - 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 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 + 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 + 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 - 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, 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 + 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 cd7e184cda..34bb0e2995 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -252,9 +252,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 +285,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 @@ -311,6 +313,80 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_strptime + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_nondefault_time_zone + with_tz_default ActiveSupport::TimeZone['Pacific Time (US & Canada)'] do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + end + + def test_strptime_with_explicit_time_zone_as_abbrev + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 PST', '%Y-%m-%d %H:%M:%S %Z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_h_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08', '%Y-%m-%d %H:%M:%S %:::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hm_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00', '%Y-%m-%d %H:%M:%S %:z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hms_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00:00', '%Y-%m-%d %H:%M:%S %::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_almost_explicit_time_zone + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 %Z', '%Y-%m-%d %H:%M:%S %%Z') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_day_omitted + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.local(2000, 2, 1), zone.strptime('Feb', '%b', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 1), zone.strptime('Feb 2005', '%b %Y', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 2), zone.strptime('2 Feb 2005', '%e %b %Y', Time.local(2000, 1, 1)) + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) @@ -402,8 +478,7 @@ class TimeZoneTest < ActiveSupport::TestCase end def test_unknown_zones_dont_store_mapping_keys - ActiveSupport::TimeZone["bogus"] - assert !ActiveSupport::TimeZone.zones_map.key?("bogus") + assert_nil ActiveSupport::TimeZone["bogus"] end def test_new @@ -414,4 +489,13 @@ class TimeZoneTest < ActiveSupport::TestCase assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"]) assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"]) end + + def test_to_yaml + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml) + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml) + end + + def test_yaml_load + assert_equal(ActiveSupport::TimeZone["Pacific/Honolulu"], YAML.load("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n")) + end end diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index 6833ae68a7..378421fedd 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_support/inflector/transliterate' diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index f49431cbbf..0e4e7427d2 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'active_support/xml_mini' require 'active_support/builder' -require 'active_support/core_ext/array' require 'active_support/core_ext/hash' require 'active_support/core_ext/big_decimal' @@ -11,7 +10,7 @@ module XmlMiniTest assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key") end - def test_rename_key_does_nothing_with_dasherize_true + def test_rename_key_dasherizes_with_dasherize_true assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => true) end 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/CHANGELOG.md b/guides/CHANGELOG.md index cd3fa9f65b..fd177b4238 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,5 +1,17 @@ +* New section in Configuring: Configuring Active Job + + *Eliot Sykes* + +* New section in Active Record Association Basics: Single Table Inheritance + + *Andrey Nering* + * New section in Active Record Querying: Understanding The Method Chaining *Andrey Nering* +* New section in Configuring: Search Engines Indexing + + *Andrey Nering* + Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/Rakefile b/guides/Rakefile index fd093b94c1..3c2099ac02 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -11,7 +11,7 @@ namespace :guides do ruby "rails_guides.rb" end - desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/kindlepublishing" + desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211" task :kindle do unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/ abort "Please `gem install kindlerb` and make sure you have `kindlegen` in your PATH" diff --git a/guides/assets/images/favicon.ico b/guides/assets/images/favicon.ico Binary files differindex e0e80cf8f1..faa10b4580 100644 --- a/guides/assets/images/favicon.ico +++ b/guides/assets/images/favicon.ico diff --git a/guides/assets/images/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png Binary files differindex 117a78a39f..c489e4c00e 100644 --- a/guides/assets/images/getting_started/article_with_comments.png +++ b/guides/assets/images/getting_started/article_with_comments.png diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index e04d79c818..11561c55f9 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -1,7 +1,17 @@ -# 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' class TestApp < Rails::Application @@ -27,7 +37,6 @@ class TestController < ActionController::Base end require 'minitest/autorun' -require 'rack/test' # Ensure backward compatibility with Minitest 4 Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 9be8130884..66887398b9 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 diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index 66bbb15afb..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' @@ -12,10 +24,10 @@ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:' ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do - create_table :posts do |t| + create_table :posts, force: true do |t| end - create_table :comments do |t| + create_table :comments, force: true do |t| t.integer :post_id end end diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index d95354e12d..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' @@ -21,10 +21,10 @@ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:' ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do - create_table :posts do |t| + create_table :posts, force: true do |t| end - create_table :comments do |t| + create_table :comments, force: true do |t| t.integer :post_id end end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb new file mode 100644 index 0000000000..a4fe51156d --- /dev/null +++ b/guides/bug_report_templates/generic_gem.rb @@ -0,0 +1,25 @@ +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' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb new file mode 100644 index 0000000000..b6b4562751 --- /dev/null +++ b/guides/bug_report_templates/generic_master.rb @@ -0,0 +1,26 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' +end + +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 762ab1c0e2..367ed0b12e 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -1,13 +1,6 @@ pwd = File.dirname(__FILE__) $:.unshift pwd -# This is a predicate useful for the doc:guides task of applications. -def bundler? - # Note that rake sets the cwd to the one that contains the Rakefile - # being executed. - File.exist?('Gemfile') -end - begin # Guides generation in the Rails repo. as_lib = File.join(pwd, "../activesupport/lib") @@ -20,44 +13,5 @@ rescue LoadError gem "actionpack", '>= 3.0' end -begin - require 'redcarpet' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Redcarpet 3.2.2+.') - $stderr.puts(<<ERROR) if bundler? -Please add - - gem 'redcarpet', '~> 3.2.2' - -to the Gemfile, run - - bundle install - -and try again. -ERROR - exit 1 -end - -begin - require 'nokogiri' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Nokogiri.') - $stderr.puts(<<ERROR) if bundler? -Please add - - gem 'nokogiri' - -to the Gemfile, run - - bundle install - -and try again. -ERROR - exit 1 -end - -require 'rails_guides/markdown' require "rails_guides/generator" RailsGuides::Generator.new.generate diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index aa900454c8..43f6f7eecf 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -57,6 +57,7 @@ require 'active_support/core_ext/object/blank' require 'action_controller' require 'action_view' +require 'rails_guides/markdown' require 'rails_guides/indexer' require 'rails_guides/helpers' require 'rails_guides/levenshtein' diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 8a908a4339..049f633258 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -7,19 +7,20 @@ module RailsGuides t = str2 n = s.length m = t.length - max = n/2 return m if (0 == n) return n if (0 == m) - return n if (n - m).abs > max d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 50a791cda5..554d94ad50 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -25,7 +25,7 @@ HTML def paragraph(text) if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ convert_notes(text) - elsif text.include?('DO NOT READ THIS FILE IN GITHUB') + elsif text.include?('DO NOT READ THIS FILE ON GITHUB') elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/ linkback = %(<a href="#footnote-#{$1}-ref"><sup>#{$1}</sup></a>) %(<p class="footnote" id="footnote-#{$1}">#{linkback} #{$2}</p>) diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index 019da08687..be00087f63 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 2.2 Release Notes =============================== diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 4ac1529e76..0a62f34371 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 2.3 Release Notes =============================== @@ -187,7 +187,7 @@ MySQL supports a reconnect flag in its connections - if set to true, then the cl * Lead Contributor: [Dov Murik](http://twitter.com/dubek) * More information: - * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html) + * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html) * [MySQL auto-reconnect revisited](http://groups.google.com/group/rubyonrails-core/browse_thread/thread/49d2a7e9c96cb9f4) ### Other Active Record Changes diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 3d7966e50b..9ad32e8168 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 3.0 Release Notes =============================== diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 8728750966..d753346fa3 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 3.1 Release Notes =============================== @@ -174,7 +174,7 @@ Rails Architectural Changes The major change in Rails 3.1 is the Assets Pipeline. It makes CSS and JavaScript first-class code citizens and enables proper organization, including use in plugins and engines. -The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. +The assets pipeline is powered by [Sprockets](https://github.com/rails/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. ### HTTP Streaming @@ -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 0b28aac9ce..6ddf77d9c0 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 3.2 Release Notes =============================== diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 5f52c33746..9feaff098a 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 4.0 Release Notes =============================== @@ -59,25 +59,25 @@ Major Features ### Upgrade - * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required - * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. - * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. - * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. - * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. - * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. - * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. - * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. +* **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required +* **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. +* **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. +* **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. +* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. +* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. +* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. +* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. ### ActionPack - * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). - * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). - * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. - * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation - * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. - * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. - * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. - * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. +* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). +* **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). +* **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. +* **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. +* **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. +* **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. +* **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. +* **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. ### General @@ -87,14 +87,17 @@ Major Features * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important. * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - Use Dalli memcache client for the memcache store. * **Notifications start & finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers. - * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe. + * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. + +NOTE: Check that the gems you are using are threadsafe. + * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources. ### Security - * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. - * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. - * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). +* **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. +* **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. +* **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). Extraction of features to gems --------------------------- @@ -181,7 +184,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a * `String#to_date` now raises `ArgumentError: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass` when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as: - ``` + ```ruby # ActiveSupport 3.x "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index dbc151c0ca..6bf65757ec 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 4.1 Release Notes =============================== @@ -317,15 +317,15 @@ for detailed changes. * Removed deprecated constants from Action Controller: - | Removed | Successor | - |:-----------------------------------|:--------------------------------| - | ActionController::AbstractRequest | ActionDispatch::Request | - | ActionController::Request | ActionDispatch::Request | - | ActionController::AbstractResponse | ActionDispatch::Response | - | ActionController::Response | ActionDispatch::Response | - | ActionController::Routing | ActionDispatch::Routing | - | ActionController::Integration | ActionDispatch::Integration | - | ActionController::IntegrationTest | ActionDispatch::IntegrationTest | +| Removed | Successor | +|:-----------------------------------|:--------------------------------| +| ActionController::AbstractRequest | ActionDispatch::Request | +| ActionController::Request | ActionDispatch::Request | +| ActionController::AbstractResponse | ActionDispatch::Response | +| ActionController::Response | ActionDispatch::Response | +| ActionController::Routing | ActionDispatch::Routing | +| ActionController::Integration | ActionDispatch::Integration | +| ActionController::IntegrationTest | ActionDispatch::IntegrationTest | ### Notable changes diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index faff1add9f..684bd286bc 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails 4.2 Release Notes =============================== @@ -257,7 +257,7 @@ application is using any of these spellings, you will need to update them: * Values in attribute selectors may need to be quoted if they contain non-alphanumeric characters. - ``` + ```ruby # before a[href=/] a[href$=/] @@ -272,7 +272,7 @@ application is using any of these spellings, you will need to update them: For example: - ``` ruby + ```ruby # content: <div><i><p></i></div> # before: @@ -290,7 +290,7 @@ application is using any of these spellings, you will need to update them: used to be raw (e.g. `AT&T`), and now is evaluated (e.g. `AT&T`). - ``` ruby + ```ruby # content: <p>AT&T</p> # before: @@ -302,6 +302,30 @@ application is using any of these spellings, you will need to update them: assert_select('p', 'AT&T') # => false ``` +Furthermore substitutions have changed syntax. + +Now you have to use a `:match` CSS-like selector: + +```ruby +assert_select ":match('id', ?)", 'comment_1' +``` + +Additionally Regexp substitutions look different when the assertion fails. +Notice how `/hello/` here: + +```ruby +assert_select(":match('id', ?)", /hello/) +``` + +becomes `"(?-mix:hello)"`: + +``` +Expected at least 1 element matching "div:match('id', "(?-mix:hello)")", found 0.. +Expected 0 to be >= 1. +``` + +See the [Rails Dom Testing](https://github.com/rails/rails-dom-testing/tree/8798b9349fb9540ad8cb9a0ce6cb88d1384a210b) documentation for more on `assert_select`. + Railties -------- diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 36d1b6de83..7d95d4792e 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Action Controller Overview ========================== @@ -9,7 +9,7 @@ After reading this guide, you will know: * How to follow the flow of a request through a controller. * How to restrict parameters passed to your controller. -* Why and how to store data in the session or cookies. +* How and why to store data in the session or cookies. * How to work with filters to execute code during request processing. * How to use Action Controller's built-in HTTP authentication. * How to stream data directly to the user's browser. @@ -21,11 +21,11 @@ After reading this guide, you will know: What Does a Controller Do? -------------------------- -Action Controller is the C in MVC. After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. +Action Controller is the C in MVC. After routing has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. For most conventional [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. -A controller can thus be thought of as a middle man between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates data from the user to the model. +A controller can thus be thought of as a middleman between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates user data to the model. NOTE: For more details on the routing process, see [Rails Routing from the Outside In](routing.html). @@ -34,7 +34,7 @@ Controller Naming Convention The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on. -Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and keeps URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. +Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form. @@ -51,7 +51,7 @@ class ClientsController < ApplicationController end ``` -As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and run the `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: +As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: ```ruby def new @@ -63,7 +63,7 @@ The [Layouts & Rendering Guide](layouts_and_rendering.html) explains this in mor `ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the API documentation or in the source itself. -Only public methods are callable as actions. It is a best practice to lower the visibility of methods which are not intended to be actions, like auxiliary methods or filters. +Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with `private` or `protected`) which are not intended to be actions, like auxiliary methods or filters. Parameters ---------- @@ -104,13 +104,13 @@ end ### Hash and Array Parameters -The `params` hash is not limited to one-dimensional keys and values. It can contain arrays and (nested) hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: +The `params` hash is not limited to one-dimensional keys and values. It can contain nested arrays and hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: ``` GET /clients?ids[]=1&ids[]=2&ids[]=3 ``` -NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as "[" and "]" are not allowed in URLs. Most of the time you don't have to worry about this because the browser will take care of it for you, and Rails will decode it back when it receives it, but if you ever find yourself having to send those requests to the server manually you have to keep this in mind. +NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as the "[" and "]" characters are not allowed in URLs. Most of the time you don't have to worry about this because the browser will encode it for you, and Rails will decode it automatically, but if you ever find yourself having to send those requests to the server manually you should keep this in mind. The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type. @@ -118,7 +118,7 @@ NOTE: Values such as `[nil]` or `[nil, nil, ...]` in `params` are replaced with `[]` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) for more information. -To send a hash you include the key name inside the brackets: +To send a hash, you include the key name inside the brackets: ```html <form accept-charset="UTF-8" action="/clients" method="post"> @@ -131,11 +131,11 @@ To send a hash you include the key name inside the brackets: When this form is submitted, the value of `params[:client]` will be `{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }`. Note the nested hash in `params[:client][:address]`. -Note that the `params` hash is actually an instance of `ActiveSupport::HashWithIndifferentAccess`, which acts like a hash but lets you use symbols and strings interchangeably as keys. +The `params` object acts like a Hash, but lets you use symbols and strings interchangeably as keys. ### JSON parameters -If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically convert your parameters into the `params` hash, which you can access as you would normally. +If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically load your parameters into the `params` hash, which you can access as you would normally. So for example, if you are sending this JSON content: @@ -143,15 +143,15 @@ So for example, if you are sending this JSON content: { "company": { "name": "acme", "address": "123 Carrot Street" } } ``` -You'll get `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. +Your controller will receive `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. -Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as: +Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON POST can be written as: ```json { "name": "acme", "address": "123 Carrot Street" } ``` -And assume that you're sending the data to `CompaniesController`, it would then be wrapped in `:company` key like this: +And, assuming that you're sending the data to `CompaniesController`, it would then be wrapped within the `:company` key like this: ```ruby { name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } } @@ -159,17 +159,17 @@ And assume that you're sending the data to `CompaniesController`, it would then You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html) -NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser` +NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser`. ### Routing Parameters -The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: +The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: ```ruby get '/clients/:status' => 'clients#index', foo: 'bar' ``` -In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar" just like it was passed in the query string. In the same way `params[:action]` will contain "index". +In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". ### `default_url_options` @@ -183,21 +183,21 @@ class ApplicationController < ActionController::Base 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 in `url_for` calls. +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 would be used for all URL generation. The method can also be defined in one specific controller, in which case it only affects URLs generated there. +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. ### Strong Parameters With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been -whitelisted. This means you'll have to make a conscious choice about -which attributes to allow for mass updating and thus prevent -accidentally exposing that which shouldn't be exposed. +whitelisted. This means that you'll have to make a conscious decision about +which attributes to allow for mass update. This is a better security +practice to help prevent accidentally allowing users to update sensitive +model attributes. -In addition, parameters can be marked as required and flow through a -predefined raise/rescue flow to end up as a 400 Bad Request with no -effort. +In addition, parameters can be marked as required and will flow through a +predefined raise/rescue flow to end up as a 400 Bad Request. ```ruby class PeopleController < ActionController::Base @@ -239,17 +239,17 @@ params.permit(:id) ``` the key `:id` will pass the whitelisting if it appears in `params` and -it has a permitted scalar value associated. Otherwise the key is going +it has a permitted scalar value associated. Otherwise, the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. The permitted scalar types are `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`, -`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and +`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile`, and `Rack::Test::UploadedFile`. To declare that the value in `params` must be an array of permitted -scalar values map the key to an empty array: +scalar values, map the key to an empty array: ```ruby params.permit(id: []) @@ -262,14 +262,13 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any sub-hash of it -permitted. Extreme care should be taken when using `permit!` as it -will allow all current and future model attributes to be -mass-assigned. +This will mark the `:log_entry` parameters hash and any sub-hash of it as +permitted. Extreme care should be taken when using `permit!`, as it +will allow all current and future model attributes to be mass-assigned. #### Nested Parameters -You can also use permit on nested parameters, like: +You can also use `permit` on nested parameters, like: ```ruby params.permit(:name, { emails: [] }, @@ -277,19 +276,19 @@ params.permit(:name, { emails: [] }, { family: [ :name ], hobbies: [] }]) ``` -This declaration whitelists the `name`, `emails` and `friends` +This declaration whitelists the `name`, `emails`, and `friends` attributes. It is expected that `emails` will be an array of permitted -scalar values and that `friends` will be an array of resources with -specific attributes : they should have a `name` attribute (any +scalar values, and that `friends` will be an array of resources with +specific attributes: they should have a `name` attribute (any permitted scalar values allowed), a `hobbies` attribute as an array of permitted scalar values, and a `family` attribute which is restricted -to having a `name` (any permitted scalar values allowed, too). +to having a `name` (any permitted scalar values allowed here, too). #### More Examples -You want to also use the permitted attributes in the `new` +You may want to also use the permitted attributes in your `new` action. This raises the problem that you can't use `require` on the -root key because normally it does not exist when calling `new`: +root key because, normally, it does not exist when calling `new`: ```ruby # using `fetch` you can supply a default and use @@ -297,8 +296,8 @@ root key because normally it does not exist when calling `new`: params.fetch(:blog, {}).permit(:title, :author) ``` -`accepts_nested_attributes_for` allows you to update and destroy -associated records. This is based on the `id` and `_destroy` +The model class method `accepts_nested_attributes_for` allows you to +update and destroy associated records. This is based on the `id` and `_destroy` parameters: ```ruby @@ -306,7 +305,7 @@ parameters: params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy]) ``` -Hashes with integer keys are treated differently and you can declare +Hashes with integer keys are treated differently, and you can declare the attributes as if they were direct children. You get these kinds of parameters when you use `accepts_nested_attributes_for` in combination with a `has_many` association: @@ -323,13 +322,13 @@ params.require(:book).permit(:title, chapters_attributes: [:title]) #### Outside the Scope of Strong Parameters The strong parameter API was designed with the most common use cases -in mind. It is not meant as a silver bullet to handle all your -whitelisting problems. However you can easily mix the API with your +in mind. It is not meant as a silver bullet to handle all of your +whitelisting problems. However, you can easily mix the API with your own code to adapt to your situation. Imagine a scenario where you have parameters representing a product name and a hash of arbitrary data associated with that product, and -you want to whitelist the product name attribute but also the whole +you want to whitelist the product name attribute and also the whole data hash. The strong parameters API doesn't let you directly whitelist the whole of a nested hash with any keys, but you can use the keys of your nested hash to declare what to whitelist: @@ -668,11 +667,11 @@ You may notice in the above code that we're using `render xml: @users`, not `ren Filters ------- -Filters are methods that are run before, after or "around" a controller action. +Filters are methods that are run "before", "after" or "around" a controller action. Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application. -"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: +"before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: ```ruby class ApplicationController < ActionController::Base @@ -705,9 +704,9 @@ Now, the `LoginsController`'s `new` and `create` actions will work as before wit In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. -"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. +"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction: @@ -737,7 +736,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, and the `require_login` filter from above could be rewritten to use a block: ```ruby class ApplicationController < ActionController::Base @@ -750,7 +749,7 @@ class ApplicationController < ActionController::Base end ``` -Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. +Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter does not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index c586675ee5..bf3bf5d19e 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Action Mailer Basics ==================== @@ -344,7 +344,7 @@ The same format can be used to set carbon copy (Cc:) and blind carbon copy Sometimes you wish to show the name of the person instead of just their email address when they receive the email. The trick to doing that is to format the -email address in the format `"Full Name <email>"`. +email address in the format `"Full Name" <email>`. ```ruby def welcome_email(user) @@ -442,6 +442,39 @@ end Will render the HTML part using the `my_layout.html.erb` file and the text part with the usual `user_mailer.text.erb` file if it exists. +### Previewing Emails + +Action Mailer previews provide a way to see how emails look by visiting a +special URL that renders them. In the above example, the preview class for +`UserMailer` should be named `UserMailerPreview` and located in +`test/mailers/previews/user_mailer_preview.rb`. To see the preview of +`welcome_email`, implement a method that has the same name and call +`UserMailer.welcome_email`: + +```ruby +class UserMailerPreview < ActionMailer::Preview + def welcome_email + UserMailer.welcome_email(User.first) + end +end +``` + +Then the preview will be available in <http://localhost:3000/rails/mailers/user_mailer/welcome_email>. + +If you change something in `app/views/user_mailer/welcome_email.html.erb` +or the mailer itself, it'll automatically reload and render it so you can +visually see the new style instantly. A list of previews are also available +in <http://localhost:3000/rails/mailers>. + +By default, these preview classes live in `test/mailers/previews`. +This can be configured using the `preview_path` option. For example, if you +want to change it to `lib/mailer_previews`, you can configure it in +`config/application.rb`: + +```ruby +config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" +``` + ### Generating URLs in Action Mailer Views Unlike controllers, the mailer instance doesn't have any context about the @@ -470,7 +503,7 @@ You will need to use: By using the full URL, your links will now work in your emails. -#### generating URLs with `url_for` +#### Generating URLs with `url_for` `url_for` generate full URL by default in templates. @@ -484,7 +517,7 @@ If you did not configure the `:host` option globally make sure to pass it to action: 'greeting') %> ``` -#### generating URLs with named routes +#### Generating URLs with Named Routes Email clients have no web context and so paths have no base URL to form complete web addresses. Thus, you should always use the "_url" variant of named route @@ -497,6 +530,9 @@ url helper. <%= user_url(@user, host: 'example.com') %> ``` +NOTE: non-`GET` links require [jQuery UJS](https://github.com/rails/jquery-ujs) +and won't work in mailer templates. They will result in normal `GET` requests. + ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index a6bde4f517..09fac41491 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Action View Overview ==================== @@ -182,7 +182,7 @@ One way to use partials is to treat them as the equivalent of subroutines; a way <p>Here are a few of our fine products:</p> <% @products.each do |product| %> - <%= render partial: "product", locals: {product: product} %> + <%= render partial: "product", locals: { product: product } %> <% end %> <%= render "shared/footer" %> @@ -190,6 +190,22 @@ One way to use partials is to treat them as the equivalent of subroutines; a way Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. +#### `render` without `partial` and `locals` options + +In the above example, `render` takes 2 options: `partial` and `locals`. But if +these are the only options you want to pass, you can skip using these options. +For example, instead of: + +```erb +<%= render partial: "product", locals: { product: @product } %> +``` + +You can also do: + +```erb +<%= render "product", product: @product %> +``` + #### The `as` and `object` options By default `ActionView::Partials::PartialRenderer` has its object in a local variable with the same name as the template. So, given: @@ -198,10 +214,11 @@ 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} %> +<%= render partial: "product", locals: { product: @product } %> ``` With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do: @@ -215,7 +232,7 @@ The `object` option can be used to directly specify which object is rendered int For example, instead of: ```erb -<%= render partial: "product", locals: {product: @item} %> +<%= render partial: "product", locals: { product: @item } %> ``` we would do: @@ -288,7 +305,7 @@ In the `show` template, we'll render the `_article` partial wrapped in the `box` **articles/show.html.erb** ```erb -<%= render partial: 'article', layout: 'box', locals: {article: @article} %> +<%= render partial: 'article', layout: 'box', locals: { article: @article } %> ``` The `box` layout simply wraps the `_article` partial in a `div`: @@ -301,26 +318,6 @@ The `box` layout simply wraps the `_article` partial in a `div`: </div> ``` -The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper: - -**articles/_article.html.erb** - -```html+erb -<%= div_for(article) do %> - <p><%= article.body %></p> -<% end %> -``` - -this would output the following: - -```html -<div class='box'> - <div id='article_1'> - <p>Partial Layouts are cool!</p> - </div> -</div> -``` - Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead: @@ -328,10 +325,10 @@ You can also render a block of code within a partial layout instead of calling ` **articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {article: @article}) do %> - <%= div_for(article) do %> +<% render(layout: 'box', locals: { article: @article }) do %> + <div> <p><%= article.body %></p> - <% end %> + </div> <% end %> ``` @@ -340,91 +337,41 @@ Supposing we use the same `_box` partial from above, this would produce the same View Paths ---------- -TODO... - -Overview of helpers provided by Action View -------------------------------------------- - -WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) - -The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point. - -### RecordTagHelper +When rendering a response, the controller needs to resolve where the different +views are located. By default it only looks inside the `app/views` directory. -This module provides methods for generating container tags, such as `div`, for your record. This is the recommended way of creating a container for render your Active Record object, as it adds an appropriate class and id attributes to that container. You can then refer to those containers easily by following the convention, instead of having to think about which class or id attribute you should use. +We can add other locations and give them a certain precedence when resolving +paths using the `prepend_view_path` and `append_view_path` methods. -#### content_tag_for +### Prepend view path -Renders a container tag that relates to your Active Record Object. +This can be helpful for example, when we want to put views inside a different +directory for subdomains. -For example, given `@article` is the object of `Article` class, you can do: +We can do this by using: -```html+erb -<%= content_tag_for(:tr, @article) do %> - <td><%= @article.title %></td> -<% end %> -``` - -This will generate this HTML output: - -```html -<tr id="article_1234" class="article"> - <td>Hello World!</td> -</tr> -``` - -You can also supply HTML attributes as an additional option hash. For example: - -```html+erb -<%= content_tag_for(:tr, @article, class: "frontpage") do %> - <td><%= @article.title %></td> -<% end %> -``` - -Will generate this HTML output: - -```html -<tr id="article_1234" class="article frontpage"> - <td>Hello World!</td> -</tr> +```ruby +prepend_view_path "app/views/#{request.subdomain}" ``` -You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects: +Then Action View will look first in this directory when resolving views. -```html+erb -<%= content_tag_for(:tr, @articles) do |article| %> - <td><%= article.title %></td> -<% end %> -``` +### Append view path -Will generate this HTML output: +Similarly, we can append paths: -```html -<tr id="article_1234" class="article"> - <td>Hello World!</td> -</tr> -<tr id="article_1235" class="article"> - <td>Ruby on Rails Rocks!</td> -</tr> +```ruby +append_view_path "app/views/direct" ``` -#### div_for +This will add `app/views/direct` to the end of the lookup paths. -This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example: - -```html+erb -<%= div_for(@article, class: "frontpage") do %> - <td><%= @article.title %></td> -<% end %> -``` +Overview of helpers provided by Action View +------------------------------------------- -Will generate this HTML output: +WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) -```html -<div id="article_1234" class="article frontpage"> - <td>Hello World!</td> -</div> -``` +The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point. ### AssetTagHelper @@ -437,39 +384,13 @@ config.action_controller.asset_host = "assets.example.com" image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> ``` -#### register_javascript_expansion - -Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"] - -javascript_include_tag :monkey # => - <script src="/assets/head.js"></script> - <script src="/assets/body.js"></script> - <script src="/assets/tail.js"></script> -``` - -#### register_stylesheet_expansion - -Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] - -stylesheet_link_tag :monkey # => - <link href="/assets/head.css" media="screen" rel="stylesheet" /> - <link href="/assets/body.css" media="screen" rel="stylesheet" /> - <link href="/assets/tail.css" media="screen" rel="stylesheet" /> -``` - #### auto_discovery_link_tag Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. ```ruby -auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # => - <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" /> +auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => + <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" /> ``` #### image_path @@ -553,7 +474,7 @@ Returns a stylesheet link tag for the sources specified as arguments. If you don stylesheet_link_tag "application" # => <link href="/assets/application.css" media="screen" rel="stylesheet" /> ``` -You can also include all styles in the stylesheet directory using :all as the source: +You can also include all styles in the stylesheet directory using `:all` as the source: ```ruby stylesheet_link_tag :all @@ -568,7 +489,7 @@ stylesheet_link_tag :all, cache: true #### stylesheet_path -Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, .css will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. +Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. ```ruby stylesheet_path "application" # => /assets/application.css @@ -612,7 +533,7 @@ end ```ruby atom_feed do |feed| feed.title("Articles Index") - feed.updated((@articles.first.created_at)) + feed.updated(@articles.first.created_at) @articles.each do |article| feed.entry(article) do |entry| @@ -786,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 @@ -850,7 +771,7 @@ time_select("order", "submitted") Returns a `pre` tag that has object dumped by YAML. This creates a very readable way to inspect an object. ```ruby -my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} +my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] } debug(my_hash) ``` @@ -869,13 +790,13 @@ third: Form helpers are designed to make working with models much easier compared to using just standard HTML elements by providing a set of methods for creating forms based on your models. This helper generates the HTML for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form is submitted (i.e., when the user hits the submit button or form.submit is called via JavaScript), the form inputs will be bundled into the params object and passed back to the controller. -There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the ActionView::Helpers::FormTagHelper documentation. +There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the `ActionView::Helpers::FormTagHelper` documentation. -The core method of this helper, form_for, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: +The core method of this helper, `form_for`, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: ```html+erb # Note: a @person variable will have been created in the controller (e.g. @person = Person.new) -<%= form_for @person, url: {action: "create"} do |f| %> +<%= form_for @person, url: { action: "create" } do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= submit_tag 'Create' %> @@ -895,7 +816,7 @@ The HTML generated for this would be: The params object created when this form is submitted would look like: ```ruby -{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}} +{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } ``` The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. @@ -913,10 +834,10 @@ check_box("article", "validated") #### fields_for -Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form: +Creates a scope around a specific model object like `form_for`, but doesn't create the form tags themselves. This makes `fields_for` suitable for specifying additional model objects in the same form: ```html+erb -<%= form_for @person, url: {action: "update"} do |person_form| %> +<%= form_for @person, url: { action: "update" } do |person_form| %> First name: <%= person_form.text_field :first_name %> Last name : <%= person_form.text_field :last_name %> @@ -1051,7 +972,7 @@ end Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true }) ``` If `@article.author_id` is 1, this would return: @@ -1207,7 +1128,7 @@ Returns a string of option tags that have been compiled by iterating over the `c # options_from_collection_for_select(collection, value_method, text_method, selected = nil) ``` -For example, imagine a loop iterating over each person in @project.people to generate an input tag: +For example, imagine a loop iterating over each person in `@project.people` to generate an input tag: ```ruby options_from_collection_for_select(@project.people, "id", "name") @@ -1223,7 +1144,7 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) ``` If `@article.person_id` is 1, this would become: @@ -1286,7 +1207,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag({action:"post"}, multipart: true) do %> +<%= form_tag({ action: "post" }, multipart: true) do %> <label for="file">File to Upload</label> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1422,22 +1343,6 @@ date_field_tag "dob" Provides functionality for working with JavaScript in your views. -#### button_to_function - -Returns a button that'll trigger a JavaScript function using the onclick handler. Examples: - -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` - -#### define_javascript_functions - -Includes the Action Pack JavaScript libraries inside a single `script` tag. - #### escape_javascript Escape carrier returns and single and double quotes for JavaScript segments. @@ -1458,15 +1363,6 @@ alert('All is good') </script> ``` -#### link_to_function - -Returns a link that will trigger a JavaScript function using the onclick handler and return false after the fact. - -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> -``` - ### NumberHelper Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. @@ -1533,7 +1429,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 tags and attributes are allowed and nothing else. ```ruby sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) @@ -1601,7 +1497,7 @@ details can be found in the [Rails Security Guide](security.html#cross-site-requ Localized Views --------------- -Action View has the ability render different templates depending on the current locale. +Action View has the ability to render different templates depending on the current locale. For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 31c9406d5c..29d0c32b09 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Job Basics ================= @@ -11,7 +11,7 @@ After reading this guide, you will know: * How to create jobs. * How to enqueue jobs. * How to run jobs in the background. -* How to send emails from your application async. +* How to send emails from your application asynchronously. -------------------------------------------------------------------------------- @@ -78,7 +78,8 @@ end Enqueue a job like so: ```ruby -# Enqueue a job to be performed as soon the queueing system is free. +# Enqueue a job to be performed as soon the queueing system is +# free. MyJob.perform_later record ``` @@ -114,8 +115,9 @@ You can easily set your queueing backend: # config/application.rb module YourApp class Application < Rails::Application - # Be sure to have the adapter's gem in your Gemfile and follow - # the adapter's specific installation and deployment instructions. + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. config.active_job.queue_adapter = :sidekiq end end @@ -153,8 +155,8 @@ class GuestsCleanupJob < ActiveJob::Base end # Now your job will run on queue production_low_priority on your -# production environment and on staging_low_priority on your staging -# environment +# production environment and on staging_low_priority +# on your staging environment ``` The default queue name prefix delimiter is '\_'. This can be changed by setting @@ -176,8 +178,8 @@ class GuestsCleanupJob < ActiveJob::Base end # Now your job will run on queue production.low_priority on your -# production environment and on staging.low_priority on your staging -# environment +# production environment and on staging.low_priority +# on your staging environment ``` If you want more control on what queue a job will be run you can pass a `:queue` @@ -203,7 +205,7 @@ class ProcessVideoJob < ActiveJob::Base end def perform(video) - # do process video + # Do process video end end @@ -236,13 +238,13 @@ class GuestsCleanupJob < ActiveJob::Base queue_as :default before_enqueue do |job| - # do something with the job instance + # Do something with the job instance end around_perform do |job, block| - # do something before perform + # Do something before perform block.call - # do something after perform + # Do something after perform end def perform @@ -295,7 +297,7 @@ end ``` This works with any class that mixes in `GlobalID::Identification`, which -by default has been mixed into Active Model classes. +by default has been mixed into Active Record classes. Exceptions @@ -309,7 +311,7 @@ class GuestsCleanupJob < ActiveJob::Base queue_as :default rescue_from(ActiveRecord::RecordNotFound) do |exception| - # do something with the exception + # Do something with the exception end def perform @@ -317,3 +319,10 @@ class GuestsCleanupJob < ActiveJob::Base end end ``` + + +Job Testing +-------------- + +You can find detailed instructions on how to test your jobs in the +[testing guide](testing.html#testing-jobs). diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 8dee1cc5ec..4b2bfaee2f 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Model Basics =================== @@ -396,7 +396,7 @@ class Person end ``` -With the `to_xml` you have a XML representing the model. +With the `to_xml` you have an XML representing the model. ```ruby person = Person.new @@ -405,7 +405,7 @@ person.name = "Bob" person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n" ``` -From a XML string you define the attributes of the model. +From an XML string you define the attributes of the model. You need to have the `attributes=` method defined on your class: ```ruby diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 9d2ba328ea..a227b54040 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Basics ==================== @@ -20,7 +20,7 @@ After reading this guide, you will know: What is Active Record? ---------------------- -Active Record is the M in [MVC](getting_started.html#the-mvc-architecture) - the +Active Record is the M in [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an @@ -38,7 +38,7 @@ object on how to write to and read from the database. ### Object Relational Mapping -Object-Relational Mapping, commonly referred to as its abbreviation ORM, is +Object Relational Mapping, commonly referred to as its abbreviation ORM, is a technique that connects the rich objects of an application to tables in a relational database management system. Using ORM, the properties and relationships of the objects in an application can be easily stored and @@ -62,7 +62,7 @@ Convention over Configuration in Active Record When writing applications using other programming languages or frameworks, it may be necessary to write a lot of configuration code. This is particularly true for ORM frameworks in general. However, if you follow the conventions adopted by -Rails, you'll need to write very little configuration (in some case no +Rails, you'll need to write very little configuration (in some cases no configuration at all) when creating Active Record models. The idea is that if you configure your applications in the very same way most of the time then this should be the default way. Thus, explicit configuration would be needed @@ -74,8 +74,8 @@ By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class `Book`, you should have a database table called **books**. The Rails -pluralization mechanisms are very powerful, being capable to pluralize (and -singularize) both regular and irregular words. When using class names composed +pluralization mechanisms are very powerful, being capable of pluralizing (and +singularizing) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: @@ -122,7 +122,7 @@ to Active Record instances: * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on - associations. For example, a `comments_count` column in a `Articles` class that + associations. For example, a `comments_count` column in an `Article` class that has many instances of `Comment` will cache the number of existent comments for each article. @@ -173,18 +173,18 @@ name that should be used: ```ruby class Product < ActiveRecord::Base - self.table_name = "PRODUCT" + self.table_name = "my_products" end ``` If you do so, you will have to define manually the class name that is hosting -the fixtures (class_name.yml) using the `set_fixture_class` method in your test +the fixtures (my_products.yml) using the `set_fixture_class` method in your test definition: ```ruby -class FunnyJoke < ActiveSupport::TestCase - set_fixture_class funny_jokes: Joke - fixtures :funny_jokes +class ProductTest < ActiveSupport::TestCase + set_fixture_class my_products: Product + fixtures :my_products ... end ``` diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 9d3a8c3af6..13989a3b33 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Callbacks ======================= @@ -68,7 +68,7 @@ class User < ActiveRecord::Base protected def normalize_name - self.name = self.name.downcase.titleize + self.name = name.downcase.titleize end def set_location diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 8ae282bad2..ad069a112e 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Migrations ======================== @@ -241,7 +241,7 @@ generates ```ruby class AddUserRefToProducts < ActiveRecord::Migration def change - add_reference :products, :user, index: true + add_reference :products, :user, index: true, foreign_key: true end end ``` @@ -357,7 +357,7 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table ### Creating a Join Table -Migration method `create_join_table` creates a HABTM join table. A typical use +Migration method `create_join_table` creates an HABTM join table. A typical use would be: ```ruby @@ -425,7 +425,7 @@ change_column :products, :part_number, :text This changes the column `part_number` on products table to be a `:text` field. Besides `change_column`, the `change_column_null` and `change_column_default` -methods are used specifically to change the null and default values of a +methods are used specifically to change a not null constraint and default values of a column. ```ruby @@ -475,11 +475,13 @@ 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 character which is 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 -`structure.sql` are required to use composite foreign keys. +`structure.sql` are required to use composite foreign keys. See +[Schema Dumping and You](#schema-dumping-and-you). Removing a foreign key is easy as well: @@ -500,7 +502,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") ``` For more details and examples of individual methods, check the API documentation. @@ -538,6 +540,14 @@ definitions: `change_table` is also reversible, as long as the block does not call `change`, `change_default` or `remove`. +`remove_column` is reversible if you supply the column type as the third +argument. Provide the original column options too, otherwise Rails can't +recreate the column exactly when rolling back: + +```ruby +remove_column :posts, :slug, :string, null: false, default: '', index: true +``` + If you're going to need to use any other methods, you should use `reversible` or write the `up` and `down` methods instead of using the `change` method. @@ -545,7 +555,7 @@ or write the `up` and `down` methods instead of using the `change` method. Complex migrations may require processing that Active Record doesn't know how to reverse. You can use `reversible` to specify what to do when running a -migration what else to do when reverting it. For example: +migration and what else to do when reverting it. For example: ```ruby class ExampleMigration < ActiveRecord::Migration @@ -597,7 +607,7 @@ schema, and the `down` method of your migration should revert the transformations done by the `up` method. In other words, the database schema should be unchanged if you do an `up` followed by a `down`. For example, if you create a table in the `up` method, you should drop it in the `down` method. It -is wise to reverse the transformations in precisely the reverse order they were +is wise to perform the transformations in precisely the reverse order they were made in the `up` method. The example in the `reversible` section is equivalent to: ```ruby @@ -695,6 +705,10 @@ of `create_table` and `reversible`, replacing `create_table` by `drop_table`, and finally replacing `up` by `down` and vice-versa. This is all taken care of by `revert`. +NOTE: If you want to add check constraints like in the examples above, +you will have to use `structure.sql` as dump method. See +[Schema Dumping and You](#schema-dumping-and-you). + Running Migrations ------------------ @@ -943,10 +957,10 @@ that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. There is however a trade-off: `db/schema.rb` cannot express database specific -items such as triggers, or stored procedures. While in a migration you can -execute custom SQL statements, the schema dumper cannot reconstitute those -statements from the database. If you are using features like this, then you -should set the schema format to `:sql`. +items such as triggers, stored procedures or check constraints. While in a +migration you can execute custom SQL statements, the schema dumper cannot +reconstitute those statements from the database. If you are using features like +this, then you should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index fa0f31cbbd..dcc523eb0f 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record and PostgreSQL ============================ @@ -29,8 +29,8 @@ that are supported by the PostgreSQL adapter. ### Bytea -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-binary.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-binarystring.html) ```ruby # db/migrate/20140207133952_create_documents.rb @@ -49,8 +49,8 @@ Document.create payload: data ### Array -* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html) +* [type definition](http://www.postgresql.org/docs/current/static/arrays.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-array.html) ```ruby # db/migrate/20140207133952_create_books.rb @@ -83,7 +83,7 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore -* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) +* [type definition](http://www.postgresql.org/docs/current/static/hstore.html) NOTE: you need to enable the `hstore` extension to use hstore. @@ -112,8 +112,8 @@ profile.save! ### JSON -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html) ```ruby # db/migrate/20131220144913_create_events.rb @@ -138,10 +138,10 @@ Event.where("payload->>'kind' = ?", "user_renamed") ### Range Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html) +* [type definition](http://www.postgresql.org/docs/current/static/rangetypes.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-range.html) -This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects. +This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.2.2/Range.html) objects. ```ruby # db/migrate/20130923065404_create_events.rb @@ -173,7 +173,7 @@ event.ends_at # => Thu, 13 Feb 2014 ### Composite Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html) +* [type definition](http://www.postgresql.org/docs/current/static/rowtypes.html) Currently there is no special support for composite types. They are mapped to normal text columns: @@ -213,7 +213,7 @@ contact.save! ### Enumerated Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-enum.html) Currently there is no special support for enumerated types. They are mapped as normal text columns: @@ -242,9 +242,12 @@ article.save! ### UUID -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) -* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) +NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb @@ -263,10 +266,32 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` +You can use `uuid` type to define references in migrations + +```ruby +# db/migrate/20150418012400_create_blog.rb +create_table :posts, id: :uuid + +create_table :comments, id: :uuid do |t| + # t.belongs_to :post, type: :uuid + t.references :post, type: :uuid +end + +# app/models/post.rb +class Post < ActiveRecord::Base + has_many :comments +end + +# app/models/comment.rb +class Comment < ActiveRecord::Base + belongs_to :post +end +``` + ### Bit String Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-bitstring.html) ```ruby # db/migrate/20131220144913_create_users.rb @@ -289,10 +314,10 @@ user.save! ### Network Address Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-net-types.html) The types `inet` and `cidr` are mapped to Ruby -[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html) +[`IPAddr`](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html) objects. The `macaddr` type is mapped to normal text. ```ruby @@ -324,7 +349,7 @@ macbook.address ### Geometric Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-geometric.html) All geometric types, with the exception of `points` are mapped to normal text. A point is casted to an array containing `x` and `y` coordinates. @@ -333,12 +358,13 @@ A point is casted to an array containing `x` and `y` coordinates. UUID Primary Keys ----------------- -NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. +NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb -enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') -create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| t.string :kind end @@ -378,7 +404,7 @@ Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", Database Views -------------- -* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html) +* [view creation](http://www.postgresql.org/docs/current/static/sql-createview.html) Imagine you need to work with a legacy database containing the following table: diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index f8c64cbd0c..e3cfabb327 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Query Interface ============================= @@ -257,6 +257,12 @@ It is equivalent to writing: Client.where(first_name: 'Lifo').take ``` +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1 +``` + The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example: ```ruby @@ -311,7 +317,7 @@ end The `find_each` method accepts most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_each`. -Two additional options, `:batch_size` and `:start`, are available as well. +Three additional options, `:batch_size`, `:begin_at` and `:end_at`, are available as well. **`:batch_size`** @@ -323,19 +329,32 @@ User.find_each(batch_size: 5000) do |user| end ``` -**`:start`** +**`:begin_at`** -By default, records are fetched in ascending order of the primary key, which must be an integer. The `:start` option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This would be useful, for example, if you wanted to resume an interrupted batch process, provided you saved the last processed ID as a checkpoint. +By default, records are fetched in ascending order of the primary key, which must be an integer. The `:begin_at` option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This would be useful, for example, if you wanted to resume an interrupted batch process, provided you saved the last processed ID as a checkpoint. For example, to send newsletters only to users with the primary key starting from 2000, and to retrieve them in batches of 5000: ```ruby -User.find_each(start: 2000, batch_size: 5000) do |user| +User.find_each(begin_at: 2000, batch_size: 5000) do |user| NewsMailer.weekly(user).deliver_now end ``` -Another example would be if you wanted multiple workers handling the same processing queue. You could have each worker handle 10000 records by setting the appropriate `:start` option on each worker. +Another example would be if you wanted multiple workers handling the same processing queue. You could have each worker handle 10000 records by setting the appropriate `:begin_at` option on each worker. + +**`:end_at`** + +Similar to the `:begin_at` option, `:end_at` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need. +This would be useful, for example, if you wanted to run a batch process, using a subset of records based on `:begin_at` and `:end_at` + +For example, to send newsletters only to users with the primary key starting from 2000 up to 10000 and to retrieve them in batches of 5000: + +```ruby +User.find_each(begin_at: 2000, end_at: 10000, batch_size: 5000) do |user| + NewsMailer.weekly(user).deliver_now +end +``` #### `find_in_batches` @@ -350,7 +369,7 @@ end ##### Options for `find_in_batches` -The `find_in_batches` method accepts the same `:batch_size` and `:start` options as `find_each`. +The `find_in_batches` method accepts the same `:batch_size`, `:begin_at` and `:end_at` options as `find_each`. Conditions ---------- @@ -510,7 +529,7 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") ``` -If you want to call `order` multiple times e.g. in different context, new order will append previous one +If you want to call `order` multiple times e.g. in different context, new order will append previous one: ```ruby Client.order("orders_count ASC").order("created_at DESC") @@ -636,7 +655,7 @@ GROUP BY status Having ------ -SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `:having` option to the find. +SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `having` method to the find. For example: @@ -879,7 +898,7 @@ For example: Item.transaction do i = Item.lock.first i.name = 'Jones' - i.save + i.save! end ``` @@ -1128,7 +1147,7 @@ This would generate a query which contains a `LEFT OUTER JOIN` whereas the If there was no `where` condition, this would generate the normal set of two queries. NOTE: Using `where` like this will only work when you pass it a Hash. For -SQL-fragments you need use `references` to force joined tables: +SQL-fragments you need to use `references` to force joined tables: ```ruby Article.includes(:comments).where("comments.visible = true").references(:comments) @@ -1269,7 +1288,7 @@ User.active.where(state: 'finished') # SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished' ``` -If we do want the `last where clause` to win then `Relation#merge` can +If we do want the last `where` clause to win then `Relation#merge` can be used. ```ruby @@ -1324,7 +1343,7 @@ Client.unscoped { Dynamic Finders --------------- -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` method. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` @@ -1384,8 +1403,9 @@ WHERE people.name = 'John' LIMIT 1 ``` -NOTE: Remember that, if `find_by` returns more than one registry, it will take -just the first and ignore the others. Note the `LIMIT 1` statement above. +NOTE: Note that if a query matches multiple records, `find_by` will +fetch only the first one and ignore the others (see the `LIMIT 1` +statement above). Find or Build a New Object -------------------------- @@ -1767,8 +1787,9 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.` under MySQL. -Active Record performs a pretty printing that emulates the one of the database -shells. So, the same query running with the PostgreSQL adapter would yield instead +Active Record performs a pretty printing that emulates that of the +corresponding database shell. So, the same query running with the +PostgreSQL adapter would yield instead ``` EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 8c832bafff..7932853c11 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Validations ========================= @@ -47,7 +47,7 @@ built-in helpers for common needs, and allows you to create your own validation methods as well. There are several other ways to validate data before it is saved into your -database, including native database constraints, client-side validations, +database, including native database constraints, client-side validations and controller-level validations. Here's a summary of the pros and cons: * Database constraints and/or stored procedures make the validation mechanisms @@ -122,7 +122,7 @@ database only if the object is valid: * `update!` The bang versions (e.g. `save!`) raise an exception if the record is invalid. -The non-bang versions don't, `save` and `update` return `false`, +The non-bang versions don't: `save` and `update` return `false`, and `create` just returns the object. ### Skipping Validations @@ -143,7 +143,7 @@ database regardless of its validity. They should be used with caution. * `update_counters` Note that `save` also has the ability to skip validations if passed `validate: -false` as argument. This technique should be used with caution. +false` as an argument. This technique should be used with caution. * `save(validate: false)` @@ -227,8 +227,26 @@ end ``` We'll cover validation errors in greater depth in the [Working with Validation -Errors](#working-with-validation-errors) section. For now, let's turn to the -built-in validation helpers that Rails provides by default. +Errors](#working-with-validation-errors) section. + +### `errors.details` + +To check which validations failed on an invalid attribute, you can use +`errors.details[:attribute]`. It returns an array of hashes with an `:error` +key to get the symbol of the validator: + +```ruby +class Person < ActiveRecord::Base + validates :name, presence: true +end + +>> person = Person.new +>> person.valid? +>> person.errors.details[:name] #=> [{error: :blank}] +``` + +Using `details` with custom validators is covered in the [Working with +Validation Errors](#working-with-validation-errors) section. Validation Helpers ------------------ @@ -254,7 +272,7 @@ available helpers. This method validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your -application's terms of service, confirm reading some text, or any similar +application's terms of service, confirm that some text is read, or any similar concept. This validation is very specific to web applications and this 'acceptance' does not need to be recorded anywhere in your database (if you don't have a field for it, the helper will just create a virtual attribute). @@ -265,6 +283,7 @@ class Person < ActiveRecord::Base end ``` +This check is performed only if `terms_of_service` is not `nil`. The default error message for this helper is _"must be accepted"_. It can receive an `:accept` option, which determines the value that will be @@ -320,7 +339,7 @@ In your view template you could use something like This check is performed only if `email_confirmation` is not `nil`. To require confirmation, make sure to add a presence check for the confirmation attribute -(we'll take a look at `presence` later on this guide): +(we'll take a look at `presence` later on in this guide): ```ruby class Person < ActiveRecord::Base @@ -452,7 +471,7 @@ point number. To specify that only integral numbers are allowed set If you set `:only_integer` to `true`, then it will use the ```ruby -/\A[+-]?\d+\Z/ +/\A[+-]?\d+\z/ ``` regular expression to validate the attribute's value. Otherwise, it will try to @@ -481,9 +500,9 @@ constraints to acceptable values: default error message for this option is _"must be equal to %{count}"_. * `:less_than` - Specifies the value must be less than the supplied value. The default error message for this option is _"must be less than %{count}"_. -* `:less_than_or_equal_to` - Specifies the value must be less than or equal the - supplied value. The default error message for this option is _"must be less - than or equal to %{count}"_. +* `:less_than_or_equal_to` - Specifies the value must be less than or equal to + the supplied value. The default error message for this option is _"must be + less than or equal to %{count}"_. * `:odd` - Specifies the value must be an odd number if set to true. The default error message for this option is _"must be odd"_. * `:even` - Specifies the value must be an even number if set to true. The @@ -533,7 +552,6 @@ Since `false.blank?` is true, if you want to validate the presence of a boolean field you should use one of the following validations: ```ruby -validates :boolean_field_name, presence: true validates :boolean_field_name, inclusion: { in: [true, false] } validates :boolean_field_name, exclusion: { in: [nil] } ``` @@ -588,9 +606,7 @@ This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, -you must create a unique index on both columns in your database. See -[the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) -for more details about multiple column indexes. +you must create a unique index on that column in your database. ```ruby class Account < ActiveRecord::Base @@ -601,7 +617,7 @@ end The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute. -There is a `:scope` option that you can use to specify other attributes that +There is a `:scope` option that you can use to specify one or more attributes that are used to limit the uniqueness check: ```ruby @@ -610,6 +626,7 @@ class Holiday < ActiveRecord::Base message: "should happen once per year" } end ``` +Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns. There is also a `:case_sensitive` option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to @@ -796,7 +813,7 @@ end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -There is also an ability to pass custom exception to `:strict` option. +There is also the ability to pass a custom exception to the `:strict` option. ```ruby class Person < ActiveRecord::Base @@ -860,7 +877,7 @@ end ### Grouping Conditional validations -Sometimes it is useful to have multiple validations use one condition, it can +Sometimes it is useful to have multiple validations use one condition. It can be easily achieved using `with_options`. ```ruby @@ -872,8 +889,8 @@ class User < ActiveRecord::Base end ``` -All validations inside of `with_options` block will have automatically passed -the condition `if: :is_admin?` +All validations inside of the `with_options` block will have automatically +passed the condition `if: :is_admin?` ### Combining Validation Conditions @@ -900,8 +917,8 @@ write your own validators or validation methods as you prefer. ### Custom Validators -Custom validators are classes that extend `ActiveModel::Validator`. These -classes must implement a `validate` method which takes a record as an argument +Custom validators are classes that inherit from `ActiveModel::Validator`. These +classes must implement the `validate` method which takes a record as an argument and performs the validation on it. The custom validator is called using the `validates_with` method. @@ -1038,7 +1055,9 @@ person.errors[:name] ### `errors.add` -The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself. +The `add` method lets you add an error message related to a particular attribute. It takes as arguments the attribute and the error message. + +The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below. ```ruby class Person < ActiveRecord::Base @@ -1056,12 +1075,12 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -Another way to do this is using `[]=` setter +An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: ```ruby class Person < ActiveRecord::Base def a_method_used_for_validation_purposes - errors[:name] = "cannot contain the characters !@#%*()_-+=" + errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" end end @@ -1074,6 +1093,43 @@ Another way to do this is using `[]=` setter # => ["Name cannot contain the characters !@#%*()_-+="] ``` +### `errors.details` + +You can specify a validator type to the returned error details hash using the +`errors.add` method. + +```ruby +class Person < ActiveRecord::Base + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters) + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters}] +``` + +To improve the error details to contain the unallowed characters set for instance, +you can pass additional keys to `errors.add`. + +```ruby +class Person < ActiveRecord::Base + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=") + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters, not_allowed: "!@#%*()_-+="}] +``` + +All built in Rails validators populate the details hash with the corresponding +validator type. + ### `errors[:base]` You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can simply add a string to it and it will be used as an error message. diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index ba839e1723..01bf928407 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Support Core Extensions ============================== @@ -349,7 +349,7 @@ end we get: ```ruby -current_user.to_query('user') # => user=357-john-smith +current_user.to_query('user') # => "user=357-john-smith" ``` This method escapes whatever is needed, both for the key and the value: @@ -467,7 +467,7 @@ C.new(0, 1).instance_variable_names # => ["@x", "@y"] NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. -### Silencing Warnings, Streams, and Exceptions +### Silencing Warnings and Exceptions The methods `silence_warnings` and `enable_warnings` change the value of `$VERBOSE` accordingly for the duration of their block, and reset it afterwards: @@ -475,26 +475,10 @@ The methods `silence_warnings` and `enable_warnings` change the value of `$VERBO silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -You can silence any stream while a block runs with `silence_stream`: +Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is not captured: ```ruby -silence_stream(STDOUT) do - # STDOUT is silent here -end -``` - -The `quietly` method addresses the common use case where you want to silence STDOUT and STDERR, even in subprocesses: - -```ruby -quietly { system 'bundle install' } -``` - -For example, the railties test suite uses that one in a few places to prevent command messages from being echoed intermixed with the progress status. - -Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is reraised: - -```ruby -# If the user is locked the increment is lost, no big deal. +# If the user is locked, the increment is lost, no big deal. suppress(ActiveRecord::StaleObjectError) do current_user.increment! :visits end @@ -522,6 +506,8 @@ Extensions to `Module` ### `alias_method_chain` +**This method is deprecated in favour of using Module#prepend.** + Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`: @@ -566,8 +552,6 @@ ActionController::TestCase.class_eval do end ``` -Rails uses `alias_method_chain` all over the code base. For example validations are added to `ActiveRecord::Base#save` by wrapping the method that way in a separate module specialized in validations. - NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. ### Attributes @@ -743,7 +727,7 @@ NOTE: Defined in `active_support/core_ext/module/introspection.rb`. #### Qualified Constant Names -The standard methods `const_defined?`, `const_get` , and `const_set` accept +The standard methods `const_defined?`, `const_get`, and `const_set` accept bare constant names. Active Support extends this API to be able to pass relative qualified constant names. @@ -1253,7 +1237,7 @@ Calling `dup` or `clone` on safe strings yields safe strings. The method `remove` will remove all occurrences of the pattern: ```ruby -"Hello World".remove(/Hello /) => "World" +"Hello World".remove(/Hello /) # => "World" ``` There's also the destructive version `String#remove!`. @@ -1952,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. @@ -2198,6 +2184,27 @@ to_visit << node if visited.exclude?(node) NOTE: Defined in `active_support/core_ext/enumerable.rb`. +### `without` + +The method `without` returns a copy of an enumerable with the specified elements +removed: + +```ruby +["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"] +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + +### `pluck` + +The method `pluck` returns an array based on the given key: + +```ruby +[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"] +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + Extensions to `Array` --------------------- @@ -2206,14 +2213,14 @@ Extensions to `Array` Active Support augments the API of arrays to ease certain ways of accessing them. For example, `to` returns the subarray of elements up to the one at the passed index: ```ruby -%w(a b c d).to(2) # => %w(a b c) +%w(a b c d).to(2) # => ["a", "b", "c"] [].to(7) # => [] ``` Similarly, `from` returns the tail from the element at the passed index to the end. If the index is greater than the length of the array, it returns an empty array. ```ruby -%w(a b c d).from(2) # => %w(c d) +%w(a b c d).from(2) # => ["c", "d"] %w(a b c d).from(10) # => [] [].from(0) # => [] ``` @@ -2221,7 +2228,7 @@ Similarly, `from` returns the tail from the element at the passed index to the e The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. ```ruby -%w(a b c d).third # => c +%w(a b c d).third # => "c" %w(a b c d).fifth # => nil ``` @@ -2234,7 +2241,7 @@ NOTE: Defined in `active_support/core_ext/array/access.rb`. This method is an alias of `Array#unshift`. ```ruby -%w(a b c d).prepend('e') # => %w(e a b c d) +%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"] [].prepend(10) # => [10] ``` @@ -2245,8 +2252,8 @@ NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. This method is an alias of `Array#<<`. ```ruby -%w(a b c d).append('e') # => %w(a b c d e) -[].append([1,2]) # => [[1,2]] +%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"] +[].append([1,2]) # => [[1, 2]] ``` NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. @@ -2433,7 +2440,7 @@ The method `Array.wrap` wraps its argument in an array unless it is already an a Specifically: -* If the argument is `nil` an empty list is returned. +* If the argument is `nil` an empty array is returned. * Otherwise, if the argument responds to `to_ary` it is invoked, and if the value of `to_ary` is not `nil`, it is returned. * Otherwise, an array with the argument as its single element is returned. @@ -2445,9 +2452,9 @@ Array.wrap(0) # => [0] This method is similar in purpose to `Kernel#Array`, but there are some differences: -* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns `nil` right away. +* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns an array with the argument as its single element right away. * If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value. -* It does not call `to_a` on the argument, though special-cases `nil` to return an empty array. +* It does not call `to_a` on the argument, if the argument does not respond to +to_ary+ it returns an array with the argument as its single element. The last point is particularly worth comparing for some enumerables: @@ -2470,7 +2477,7 @@ NOTE: Defined in `active_support/core_ext/array/wrap.rb`. ### Duplicating -The method `Array.deep_dup` duplicates itself and all objects inside +The method `Array#deep_dup` duplicates itself and all objects inside recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside. ```ruby @@ -2692,7 +2699,7 @@ NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`. ### Deep duplicating -The method `Hash.deep_dup` duplicates itself and all keys and values +The method `Hash#deep_dup` duplicates itself and all keys and values inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside. ```ruby @@ -2888,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 @@ -3045,53 +3052,6 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => #<UnboundMethod: Hash#delete> -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. - -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. - -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: - -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end - - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` - -NOTE: Defined in `active_support/core_ext/proc.rb`. - Extensions to `Date` -------------------- @@ -3813,50 +3773,6 @@ WARNING. If the argument is an `IO` it needs to respond to `rewind` to be able t NOTE: Defined in `active_support/core_ext/marshal.rb`. -Extensions to `Logger` ----------------------- - -### `around_[level]` - -Takes two arguments, a `before_message` and `after_message` and calls the current level method on the `Logger` instance, passing in the `before_message`, then the specified message, then the `after_message`: - -```ruby -logger = Logger.new("log/development.log") -logger.around_info("before", "after") { |logger| logger.info("during") } -``` - -### `silence` - -Silences every log level lesser to the specified one for the duration of the given block. Log level orders are: debug, info, error and fatal. - -```ruby -logger = Logger.new("log/development.log") -logger.silence(Logger::INFO) do - logger.debug("In space, no one can hear you scream.") - logger.info("Scream all you want, small mailman!") -end -``` - -### `datetime_format=` - -Modifies the datetime format output by the formatter class associated with this logger. If the formatter class does not have a `datetime_format` method then this is ignored. - -```ruby -class Logger::FormatWithTime < Logger::Formatter - cattr_accessor(:datetime_format) { "%Y%m%d%H%m%S" } - - def self.call(severity, timestamp, progname, msg) - "#{timestamp.strftime(datetime_format)} -- #{String === msg ? msg : msg.inspect}\n" - end -end - -logger = Logger.new("log/development.log") -logger.formatter = Logger::FormatWithTime -logger.info("<- is the current time") -``` - -NOTE: Defined in `active_support/core_ext/logger.rb`. - Extensions to `NameError` ------------------------- diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 0aa74e387d..373dbbb9aa 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Support Instrumentation ============================== @@ -19,7 +19,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the [Rails framework](#rails-framework-hooks). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. @@ -218,7 +218,7 @@ Action View ```ruby { - identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb", + identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb" } ``` @@ -252,6 +252,20 @@ INFO. The adapters will add their own data as well. | `:name` | Record's class | | `:connection_id` | `self.object_id` | +### instantiation.active_record + +| Key | Value | +| ---------------- | ----------------------------------------- | +| `:record_count` | Number of records that instantiated | +| `:class_name` | Record's class | + +```ruby +{ + record_count: 1, + class_name: "User" +} +``` + Action Mailer ------------- @@ -307,7 +321,7 @@ Action Mailer } ``` -ActiveResource +Active Resource -------------- ### request.active_resource diff --git a/guides/source/api_app.md b/guides/source/api_app.md new file mode 100644 index 0000000000..0a6335ed88 --- /dev/null +++ b/guides/source/api_app.md @@ -0,0 +1,435 @@ +Using Rails for API-only Apps +============================= + +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 + +endprologue. + +### 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 backend 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, and consuming a simple JSON API + +This guide covers building a Rails application that serves JSON +resources to an API client **or** 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 application come with smart defaults for + development, making development pleasant without compromising + production-time performance. +- Test Mode: Ditto test 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 params? 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* requests, 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, I think this list demonstrates that the default Rails +middleware stack provides a lot of value, even if you’re “just +generating JSON”. + +Handled at the ActionPack 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 3.0 added 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, params, 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 + reduces or eliminates 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 backend). + +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 ActiveRecord. + +**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: + +<shell>\ +\$ rails new my\_api --api\ +</shell> + +This will do three main things for you: + +- Configure your application to start with a more limited set of + middleware than normal. Specifically, it will not include any + middleware primarily useful for browser applications (like cookie + support) by default. +- Make *ApplicationController* inherit from *ActionController::API* + instead of *ActionController::Base*. As with middleware, this will + leave out any *ActionController* modules that provide functionality + 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 app and make it an API app, follow the +following steps. + +In *config/application.rb* add the following line at the top of the +*Application* class: + +<ruby>\ +config.api\_only!\ +</ruby> + +Change *app/controllers/application\_controller.rb*: + +<ruby> + +1. instead of\ + class ApplicationController \< ActionController::Base\ + end + +<!-- --> + +1. do\ + class ApplicationController \< ActionController::API\ + end\ + </ruby> + +### Choosing Middlewares + +An API application comes with the following middlewares by default. + +- *Rack::Cache*: Caches responses with public *Cache-Control* headers + using HTTP caching semantics. See below for more information. +- *Rack::Sendfile*: Uses a front-end server’s file serving support + from your Rails application. +- *Rack::Lock*: If your application is not marked as threadsafe + (*config.threadsafe!*), this middleware will add a mutex around your + requests. +- *ActionDispatch::RequestId*: +- *Rails::Rack::Logger*: +- *Rack::Runtime*: Adds a header to the response listing the total + runtime of the request. +- *ActionDispatch::ShowExceptions*: Rescue exceptions and re-dispatch + them to an exception handling application +- *ActionDispatch::DebugExceptions*: Log exceptions +- *ActionDispatch::RemoteIp*: Protect against IP spoofing attacks +- *ActionDispatch::Reloader*: In development mode, support code + reloading. +- *ActionDispatch::ParamsParser*: Parse XML, YAML and JSON parameters + when the request’s *Content-Type* is one of those. +- *ActionDispatch::Head*: Dispatch *HEAD* requests as *GET* requests, + and return only the status code and headers. +- *Rack::ConditionalGet*: Supports the *stale?* feature in Rails + controllers. +- *Rack::ETag*: Automatically set an *ETag* on all string responses. + This means that if the same response is returned from a controller + for the same URL, the server will return a *304 Not Modified*, even + if no additional caching steps are taken. This is primarily a + client-side optimization; it reduces bandwidth costs but not server + processing time. + +Other plugins, including *ActiveRecord*, may add additional middlewares. +In general, these middlewares are agnostic to the type of app you are +building, and make sense in an API-only Rails application. + +You can get a list of all middlewares in your application via: + +<shell>\ +\$ rake middleware\ +</shell> + +#### Using Rack::Cache + +When used with Rails, *Rack::Cache* uses the Rails cache store for its +entity and meta stores. This means that if you use memcache, for your +Rails app, for instance, the built-in HTTP cache will use memcache. + +To make use of *Rack::Cache*, you will want to use *stale?* in your +controller. Here’s an example of *stale?* in use. + +<ruby>\ +def show\ + @post = Post.find(params[:id]) + +if stale?(:last\_modified =\> `post.updated_at) + render json: `post\ + end\ +end\ +</ruby> + +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 with the response. + +Normally, this mechanism is used on a per-client basis. *Rack::Cache* +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\ +</ruby> + +This means that *Rack::Cache* will store off *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: The *Rack::Cache* middleware is always outside of the *Rack::Lock* +mutex, even in single-threaded apps. + +#### Using Rack::Sendfile + +When you use the *send\_file* method in 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 purposes using *config.action\_dispatch.x\_sendfile\_header* in +the appropriate environment config 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) + +The values for popular servers once they are configured to support +accelerated file sending: + +<ruby> + +1. Apache and lighttpd\ + config.action\_dispatch.x\_sendfile\_header = “X-Sendfile” + +<!-- --> + +1. nginx\ + config.action\_dispatch.x\_sendfile\_header = “X-Accel-Redirect”\ + </ruby> + +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 apps. + +#### Using ActionDispatch::ParamsParser + +*ActionDispatch::ParamsParser* will take parameters from the client in +JSON and make them available in your controller as *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: + +<plain>\ +jQuery.ajax({\ + type: ‘POST’,\ + url: ‘/people’\ + dataType: ‘json’,\ + contentType: ‘application/json’,\ + data: JSON.stringify({ person: { firstName: “Yehuda”, lastName: “Katz” +} }), + +success: function(json) { }\ +});\ +</plain> + +*ActionDispatch::ParamsParser* will see the *Content-Type* and your +params will be *{ :person =\> { :firstName =\> “Yehuda”, :lastName =\> +“Katz” } }*. + +#### Other Middlewares + +Rails ships with a number of other middlewares that you might want to +use in an API app, especially if one of your API clients is the browser: + +- *Rack::MethodOverride*: Allows the use of the *\_method* hack to + route POST requests to other verbs. +- *ActionDispatch::Cookies*: Supports the *cookie* method in + *ActionController*, including support for signed and encrypted + cookies. +- *ActionDispatch::Flash*: Supports the *flash* mechanism in + *ActionController*. +- *ActionDispatch::BestStandards*: Tells Internet Explorer to use the + most standards-compliant available renderer. In production mode, if + ChromeFrame is available, use ChromeFrame. +- Session Management: If a *config.session\_store* is supplied, this + middleware makes the session available as the *session* method in + *ActionController*. + +Any of these middlewares can be adding via: + +<ruby>\ +config.middleware.use Rack::MethodOverride\ +</ruby> + +#### 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 using +*config.middleware.delete*: + +<ruby>\ +config.middleware.delete ::Rack::Sendfile\ +</ruby> + +Keep in mind that removing these features may remove support for certain +features in *ActionController*. + +### 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 *ActionController* (see [the + source](https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/instrumentation.rb) + for more). +- *ActionController::Rescue*: Support for *rescue\_from*. + +Other plugins may add additional modules. You can get a list of all +modules included into *ActionController::API* in the rails console: + +<shell>\ +\$ irb\ +\>\> ActionController::API.ancestors - +ActionController::Metal.ancestors\ +</shell> + +#### Adding Other Modules + +All ActionController 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. These delegate to + *I18n.translate* and *I18n.localize*. +- *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 content negotiation + (*respond\_to*, *respond\_with*). +- *ActionController::Cookies*: Support for *cookies*, which includes + support for signed and encrypted cookies. This requires the cookie + middleware. + +The best place to add a module is in your *ApplicationController*. You +can also add modules to individual controllers. diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index d481700709..46c9013087 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** API Documentation Guidelines ============================ @@ -16,7 +16,8 @@ RDoc ---- The [Rails API documentation](http://api.rubyonrails.org) is generated with -[RDoc](http://docs.seattlerb.org/rdoc/). +[RDoc](http://docs.seattlerb.org/rdoc/). To generate it, make sure you are +in the rails root directory, run `bundle install` and execute: ```bash bundle exec rake rdoc diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 64d1c31083..4a610e8458 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** The Asset Pipeline ================== @@ -149,7 +149,7 @@ clients to fetch them again, even when the content of those assets has not chang Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content. -Fingerprinting is enabled by default for production and disabled for all other +Fingerprinting is enabled by default for both the development and production environments. You can enable or disable it in your configuration through the `config.assets.digest` option. @@ -209,7 +209,7 @@ precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows, you have a JavaScript runtime installed in -your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes. +your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the following to your `config/application.rb` configuration: @@ -643,7 +643,7 @@ above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. During the precompilation phase an MD5 is generated from the contents of the -compiled files, and inserted into the filenames as they are written to disc. +compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -667,8 +667,7 @@ anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`. The fingerprinting behavior is controlled by the `config.assets.digest` -initialization option (which defaults to `true` for production and `false` for -everything else). +initialization option (which defaults to `true` for production and development). NOTE: Under normal circumstances the default `config.assets.digest` option should not be changed. If there are no digests in the filenames, and far-future @@ -729,27 +728,6 @@ include, you can add them to the `precompile` array in `config/initializers/asse Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js'] ``` -Or, you can opt to precompile all assets with something like this: - -```ruby -# config/initializers/assets.rb -Rails.application.config.assets.precompile << Proc.new do |path| - if path =~ /\.(css|js)\z/ - full_path = Rails.application.assets.resolve(path).to_path - app_assets_path = Rails.root.join('app', 'assets').to_path - if full_path.starts_with? app_assets_path - logger.info "including asset: " + full_path - true - else - logger.info "excluding asset: " + full_path - false - end - else - false - end -end -``` - NOTE. Always specify an expected compiled filename that ends with .js or .css, even if you want to add Sass or CoffeeScript files to the precompile array. @@ -812,41 +790,6 @@ location ~ ^/assets/ { } ``` -#### GZip Compression - -When files are precompiled, Sprockets also creates a -[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web -servers are typically configured to use a moderate compression ratio as a -compromise, but since precompilation happens once, Sprockets uses the maximum -compression ratio, thus reducing the size of the data transfer to the minimum. -On the other hand, web servers can be configured to serve compressed content -directly from disk, rather than deflating non-compressed files themselves. - -NGINX is able to do this automatically enabling `gzip_static`: - -```nginx -location ~ ^/(assets)/ { - root /path/to/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; -} -``` - -This directive is available if the core module that provides this feature was -compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have -the module compiled. Otherwise, you may need to perform a manual compilation: - -```bash -./configure --with-http_gzip_static_module -``` - -If you're compiling NGINX with Phusion Passenger you'll need to pass that option -when prompted. - -A robust configuration for Apache is possible but tricky; please Google around. -(Or help update this Guide if you have a good configuration example for Apache.) - ### Local Precompilation There are several reasons why you might want to precompile your assets locally. @@ -942,7 +885,7 @@ focus on serving application code as fast as possible. #### Set up a CDN to Serve Static Assets To set up your CDN you have to have your application running in production on -the internet at a publically available URL, for example `example.com`. Next +the internet at a publicly available URL, for example `example.com`. Next you'll need to sign up for a CDN service from a cloud hosting provider. When you do this you need to configure the "origin" of the CDN to point back at your website `example.com`, check your provider for documentation on configuring the @@ -995,7 +938,7 @@ http://mycdnsubdomain.fictional-cdn.com/assets/smile.png If the CDN has a copy of `smile.png` it will serve it to the browser and your server doesn't even know it was requested. If the CDN does not have a copy it -will try to find it a the "origin" `example.com/assets/smile.png` and then store +will try to find it at the "origin" `example.com/assets/smile.png` and then store it for future use. If you want to serve only some assets from your CDN, you can use custom `:host` @@ -1158,7 +1101,7 @@ The following line invokes `uglifier` for JavaScript compression. config.assets.js_compressor = :uglifier ``` -NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme) +NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 95c7e747ef..05dd0d2a04 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Active Record Associations ========================== @@ -146,6 +146,17 @@ class CreateSuppliers < ActiveRecord::Migration end ``` +Depending on the use case, you might also need to create a unique index and/or +a foreign key constraint on the supplier column for the accounts table. In this +case, the column definition might look like this: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: true, unique: true, foreign_key: true + # ... +end +``` + ### The `has_many` Association A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this: @@ -171,7 +182,7 @@ class CreateCustomers < ActiveRecord::Migration end create_table :orders do |t| - t.belongs_to :customer, index:true + t.belongs_to :customer, index: true t.datetime :order_date t.timestamps null: false end @@ -424,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 @@ -460,7 +471,7 @@ class CreatePictures < ActiveRecord::Migration t.timestamps null: false end - add_index :pictures, :imageable_id + add_index :pictures, [:imageable_type, :imageable_id] end end ``` @@ -579,7 +590,7 @@ If you create an association some time after you build the underlying model, you If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering. -WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically _less_ than 's' in common encodings). +WARNING: The precedence between model names is calculated using the `<=>` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings). Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations: @@ -609,7 +620,7 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration 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 models IDs, or exceptions about conflicting IDs, chances are you forgot that bit. +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. ### Controlling Association Scope @@ -691,7 +702,7 @@ c.first_name = 'Manny' c.first_name == o.customer.first_name # => false ``` -This happens because c and o.customer are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: +This happens because `c` and `o.customer` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: ```ruby class Customer < ActiveRecord::Base @@ -726,10 +737,10 @@ Most associations with standard names will be supported. However, associations that contain the following options will not have their inverses set automatically: -* :conditions -* :through -* :polymorphic -* :foreign_key +* `:conditions` +* `:through` +* `:polymorphic` +* `:foreign_key` Detailed Association Reference ------------------------------ @@ -782,7 +793,7 @@ If the associated object has already been retrieved from the database for this o ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associate object and setting this object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associated object and setting this object's foreign key to the same value. ```ruby @order.customer = @customer @@ -829,10 +840,12 @@ The `belongs_to` association supports these options: * `:counter_cache` * `:dependent` * `:foreign_key` +* `:primary_key` * `:inverse_of` * `:polymorphic` * `:touch` * `:validate` +* `:optional` ##### `:autosave` @@ -874,18 +887,26 @@ end With this declaration, Rails will keep the cache value up to date, and then return that value in response to the `size` method. -Although the `:counter_cache` option is specified on the model that includes the `belongs_to` declaration, the actual column must be added to the _associated_ model. In the case above, you would need to add a column named `orders_count` to the `Customer` model. You can override the default column name if you need to: +Although the `:counter_cache` option is specified on the model that includes +the `belongs_to` declaration, the actual column must be added to the +_associated_ (`has_many`) model. In the case above, you would need to add a +column named `orders_count` to the `Customer` model. + +You can override the default column name by specifying a custom column name in +the `counter_cache` declaration instead of `true`. For example, to use +`count_of_orders` instead of `orders_count`: ```ruby class Order < ActiveRecord::Base belongs_to :customer, counter_cache: :count_of_orders end class Customer < ActiveRecord::Base - has_many :orders, counter_cache: :count_of_orders + has_many :orders end ``` -NOTE: You only need to specify the :counter_cache option on the "has_many side" of the association when using a custom name for the counter cache. +NOTE: You only need to specify the :counter_cache option on the `belongs_to` +side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. @@ -912,6 +933,26 @@ end TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +##### `:primary_key` + +By convention, Rails assumes that the `id` column is used to hold the primary key +of its tables. The `:primary_key` option allows you to specify a different column. + +For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so: + +```ruby +class User < ActiveRecord::Base + self.primary_key = 'guid' # primary key is guid and not id +end + +class Todo < ActiveRecord::Base + belongs_to :user, primary_key: 'guid' +end +``` + +When we execute `@user.todos.create` then the `@todo` record will have its +`user_id` value as the `guid` value of `@user`. + ##### `:inverse_of` The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. @@ -932,7 +973,7 @@ Passing `true` to the `:polymorphic` option indicates that this is a polymorphic ##### `:touch` -If you set the `:touch` option to `:true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: +If you set the `:touch` option to `true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: ```ruby class Order < ActiveRecord::Base @@ -956,6 +997,11 @@ end If you set the `:validate` option to `true`, then associated objects will be validated whenever you save this object. By default, this is `false`: associated objects will not be validated when this object is saved. +##### `:optional` + +If you set the `:optional` option to `true`, then the presence of the associated +object won't be validated. By default, this option is set to `false`. + #### Scopes for `belongs_to` There may be times when you wish to customize the query used by `belongs_to`. Such customizations can be achieved via a scope block. For example: @@ -1092,7 +1138,7 @@ If the associated object has already been retrieved from the database for this o ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associate object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associated object's foreign key to the same value. ```ruby @supplier.account = @account @@ -1173,8 +1219,8 @@ Controls what happens to the associated object when its owner is destroyed: It's necessary not to set or leave `:nullify` option for those associations that have `NOT NULL` database constraints. If you don't set `dependent` to destroy such associations you won't be able to change the associated object -because initial associated object foreign key will be set to unallowed `NULL` -value. +because the initial associated object's foreign key will be set to the +unallowed `NULL` value. ##### `:foreign_key` @@ -1421,7 +1467,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?` @@ -1460,7 +1513,9 @@ 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 conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +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 = {}, ...)` @@ -1490,7 +1545,7 @@ While Rails uses intelligent defaults that will work well in most situations, th ```ruby class Customer < ActiveRecord::Base - has_many :orders, dependent: :delete_all, validate: :false + has_many :orders, dependent: :delete_all, validate: false end ``` @@ -1571,9 +1626,10 @@ end By convention, Rails assumes that the column used to hold the primary key of the association is `id`. You can override this and explicitly specify the primary key with the `:primary_key` option. -Let's say that `users` table has `id` as the primary_key but it also has -`guid` column. And the requirement is that `todos` table should hold -`guid` column value and not `id` value. This can be achieved like this +Let's say the `users` table has `id` as the primary_key but it also +has a `guid` column. The requirement is that the `todos` table should +hold the `guid` column value as the foreign key and not `id` +value. This can be achieved like this: ```ruby class User < ActiveRecord::Base @@ -1581,8 +1637,8 @@ class User < ActiveRecord::Base end ``` -Now if we execute `@user.todos.create` then `@todo` record will have -`user_id` value as the `guid` value of `@user`. +Now if we execute `@todo = @user.todos.create` then the `@todo` +record's `user_id` value will be the `guid` value of `@user`. ##### `:source` @@ -1622,7 +1678,7 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` @@ -1958,7 +2014,9 @@ 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 conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +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 = {})` @@ -1986,8 +2044,8 @@ While Rails uses intelligent defaults that will work well in most situations, th ```ruby class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, autosave: true, - readonly: true + has_and_belongs_to_many :assemblies, -> { readonly }, + autosave: true end ``` @@ -1999,7 +2057,6 @@ The `has_and_belongs_to_many` association supports these options: * `:foreign_key` * `:join_table` * `:validate` -* `:readonly` ##### `:association_foreign_key` @@ -2243,3 +2300,67 @@ Extensions can refer to the internals of the association proxy using these three * `proxy_association.owner` returns the object that the association is a part of. * `proxy_association.reflection` returns the reflection object that describes the association. * `proxy_association.target` returns the associated object for `belongs_to` or `has_one`, or the collection of associated objects for `has_many` or `has_and_belongs_to_many`. + +Single Table Inheritance +------------------------ + +Sometimes, you may want to share fields and behavior between different models. +Let's say we have Car, Motorcycle and Bicycle models. We will want to share +the `color` and `price` fields and some methods for all of them, but having some +specific behavior for each, and separated controllers too. + +Rails makes this quite easy. First, let's generate the base Vehicle model: + +```bash +$ rails generate model vehicle type:string color:string price:decimal{10.2} +``` + +Did you note we are adding a "type" field? Since all models will be saved in a +single database table, Rails will save in this column the name of the model that +is being saved. In our example, this can be "Car", "Motorcycle" or "Bicycle." +STI won't work without a "type" field in the table. + +Next, we will generate the three models that inherit from Vehicle. For this, +we can use the `--parent=PARENT` option, which will generate a model that +inherits from the specified parent and without equivalent migration (since the +table already exists). + +For example, to generate the Car model: + +```bash +$ rails generate model car --parent=Vehicle +``` + +The generated model will look like this: + +```ruby +class Car < Vehicle +end +``` + +This means that all behavior added to Vehicle is available for Car too, as +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 +``` + +will generate the following SQL: + +```sql +INSERT INTO "vehicles" ("type", "color", "price") VALUES ("Car", "Red", 10000) +``` + +Querying car records will just search for vehicles that are cars: + +```ruby +Car.all +``` + +will run a query like: + +```sql +SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car') +``` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 489ea681e2..2b6d7e4044 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Autoloading and Reloading Constants =================================== @@ -80,7 +80,8 @@ end ``` The *nesting* at any given place is the collection of enclosing nested class and -module objects outwards. For example, in the previous example, the nesting at +module objects outwards. The nesting at any given place can be inspected with +`Module.nesting`. For example, in the previous example, the nesting at (1) is ```ruby @@ -113,6 +114,16 @@ certain nesting does not necessarily correlate with the namespaces at the spot. Even more, they are totally independent, take for instance ```ruby +module X + module Y + end +end + +module A + module B + end +end + module X::Y module A::B # (3) @@ -140,9 +151,10 @@ executed, and popped after it. * A singleton class opened with `class << object` gets pushed, and popped later. -* When any of the `*_eval` family of methods is called using a string argument, +* When `instance_eval` is called using a string argument, the singleton class of the receiver is pushed to the nesting of the eval'ed -code. +code. When `class_eval` or `module_eval` is called using a string argument, +the receiver is pushed to the nesting of the eval'ed code. * The nesting at the top-level of code interpreted by `Kernel#load` is empty unless the `load` call receives a true value as second argument, in which case @@ -153,8 +165,6 @@ the blocks that may be passed to `Class.new` and `Module.new` do not get the class or module being defined pushed to their nesting. That's one of the differences between defining classes and modules in one way or another. -The nesting at any given place can be inspected with `Module.nesting`. - ### Class and Module Definitions are Constant Assignments Let's suppose the following snippet creates a class (rather than reopening it): @@ -236,7 +246,7 @@ end ``` `Post` is not syntax for a class. Rather, `Post` is a regular Ruby constant. If -all is good, the constant evaluates to an object that responds to `all`. +all is good, the constant is evaluated to an object that responds to `all`. That is why we talk about *constant* autoloading, Rails has the ability to load constants on the fly. @@ -291,7 +301,9 @@ order. The ancestors of those elements are ignored. 2. If not found, then the algorithm walks up the ancestor chain of the cref. -3. If not found, `const_missing` is invoked on the cref. The default +3. If not found and the cref is a module, the constant is looked up in `Object`. + +4. If not found, `const_missing` is invoked on the cref. The default implementation of `const_missing` raises `NameError`, but it can be overridden. Rails autoloading **does not emulate this algorithm**, but its starting point is @@ -314,7 +326,7 @@ relative: `::Billing::Invoice`. That would force `Billing` to be looked up only as a top-level constant. `Invoice` on the other hand is qualified by `Billing` and we are going to see -its resolution next. Let's call *parent* to that qualifying class or module +its resolution next. Let's define *parent* to be that qualifying class or module object, that is, `Billing` in the example above. The algorithm for qualified constants goes like this: @@ -330,7 +342,7 @@ checked. Rails autoloading **does not emulate this algorithm**, but its starting point is the name of the constant to be autoloaded, and the parent. See more in -[Qualified References](#qualified-references). +[Qualified References](#autoloading-algorithms-qualified-references). Vocabulary @@ -451,9 +463,11 @@ Also, this collection is configurable via `config.autoload_paths`. For example, by adding this to `config/application.rb`: ```ruby -config.autoload_paths += "#{Rails.root}/lib" +config.autoload_paths << "#{Rails.root}/lib" ``` +`config.autoload_paths` is not changeable from environment-specific configuration files. + The value of `autoload_paths` can be inspected. In a just generated application it is (edited): @@ -685,12 +699,12 @@ creates an empty module and assigns it to the `Admin` constant on the fly. ### Generic Procedure Relative references are reported to be missing in the cref where they were hit, -and qualified references are reported to be missing in their parent. (See +and qualified references are reported to be missing in their parent (see [Resolution Algorithm for Relative Constants](#resolution-algorithm-for-relative-constants) at the beginning of this guide for the definition of *cref*, and [Resolution Algorithm for Qualified Constants](#resolution-algorithm-for-qualified-constants) for the definition of -*parent*.) +*parent*). The procedure to autoload constant `C` in an arbitrary situation is as follows: @@ -868,8 +882,8 @@ end ``` To resolve `User` Ruby checks `Admin` in the former case, but it does not in -the latter because it does not belong to the nesting. (See [Nesting](#nesting) -and [Resolution Algorithms](#resolution-algorithms).) +the latter because it does not belong to the nesting (see [Nesting](#nesting) +and [Resolution Algorithms](#resolution-algorithms)). Unfortunately Rails autoloading does not know the nesting in the spot where the constant was missing and so it is not able to act as Ruby would. In particular, @@ -1284,7 +1298,7 @@ c.user # NameError: uninitialized constant C::User ``` because it detects that a parent namespace already has the constant (see [Qualified -References](#autoloading-algorithms-qualified-references).) +References](#autoloading-algorithms-qualified-references)). As with pure Ruby, within the body of a direct descendant of `BasicObject` use always absolute constant paths: diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 61b991df61..782406659d 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Caching with Rails: An overview =============================== @@ -22,7 +22,7 @@ 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 `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. +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. ```ruby config.action_controller.perform_caching = true @@ -32,7 +32,7 @@ config.action_controller.perform_caching = true 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. -INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method. +INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). ### Action Caching @@ -144,7 +144,7 @@ It's called "Russian Doll Caching" because it nests multiple fragments. The adva ### Low-Level Caching -Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information. +Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information. The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned. @@ -160,7 +160,7 @@ class Product < ActiveRecord::Base end ``` -NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. +NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. ### SQL Caching @@ -184,6 +184,10 @@ class ProductsController < ApplicationController 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. + Cache Stores ------------ @@ -215,7 +219,7 @@ There are some common options used by all cache implementations. These can be pa * `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. -* `:compress_threshold` - This options is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. +* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. * `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache. @@ -223,7 +227,7 @@ There are some common options used by all cache implementations. These can be pa ### 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` options 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 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 } @@ -239,7 +243,7 @@ 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. Servers 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. 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. Note that the cache will grow until the disk is full unless you periodically clear out old entries. @@ -251,7 +255,7 @@ This cache store uses Danga's `memcached` server to provide a centralized cache 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. -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 operation 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. +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. ```ruby config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" @@ -289,7 +293,7 @@ For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/ ### 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. +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. ```ruby config.cache_store = :null_store @@ -299,7 +303,7 @@ config.cache_store = :null_store 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, simple set the cache store to a new instance of the class. +To use a custom cache store, simply set the cache store to a new instance of the class. ```ruby config.cache_store = MyCacheStore.new @@ -307,7 +311,7 @@ config.cache_store = MyCacheStore.new ### Cache Keys -The keys used in a cache can be any object that responds to either `:cache_key` or to `: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. @@ -349,7 +353,7 @@ class ProductsController < ApplicationController end ``` -Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 7567a38aef..0f5a9e4e39 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** The Rails Command Line ====================== @@ -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 ``` @@ -338,6 +338,12 @@ You can specify the environment in which the `runner` command should operate usi $ bin/rails runner -e staging "Model.long_running_method" ``` +You can even execute ruby code written in a file with runner. + +```bash +$ bin/rails runner lib/code_to_be_run.rb +``` + ### `rails destroy` Think of `destroy` as the opposite of `generate`. It'll figure out what generate did, and undo it. @@ -396,8 +402,8 @@ INFO: You can also use `rake -T` to get the list of tasks. $ bin/rake about About your application's environment Rails version 5.0.0 -Ruby version 2.2.0 (x86_64-linux) -RubyGems version 2.4.5 +Ruby version 2.2.2 (x86_64-linux) +RubyGems version 2.4.6 Rack version 1.6 JavaScript Runtime Node.js (V8) Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag @@ -417,15 +423,7 @@ If you want to clear `public/assets` completely, you can use `rake assets:clobbe The most common tasks of the `db:` Rake namespace are `migrate` and `create`, and it will pay off to try out all of the migration rake tasks (`up`, `down`, `redo`, `reset`). `rake db:version` is useful when troubleshooting, telling you the current version of the database. -More information about migrations can be found in the [Migrations](migrations.html) guide. - -### `doc` - -The `doc:` namespace has the tools to generate documentation for your app, API documentation, guides. Documentation can also be stripped which is mainly useful for slimming your codebase, like if you're writing a Rails application for an embedded platform. - -* `rake doc:app` generates documentation for your application in `doc/app`. -* `rake doc:guides` generates Rails guides in `doc/guides`. -* `rake doc:rails` generates API documentation for Rails in `doc/api`. +More information about migrations can be found in the [Migrations](active_record_migrations.html) guide. ### `notes` @@ -472,7 +470,7 @@ app/models/article.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rake notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' @@ -528,8 +526,8 @@ end To pass arguments to your custom rake task: ```ruby -task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args| - # You can use args from here +task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args| + argument_1 = args.arg_1 end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 5baab8a4b5..bb6c395c96 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Configuring Rails Applications ============================== @@ -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. @@ -110,9 +108,11 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. -* `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all environments. +* `config.log_level` defines the verbosity of the Rails logger. This option +defaults to `:debug` for all environments. The available log levels are: `:debug`, +`:info`, `:warn`, `:error`, `:fatal`, and `:unknown`. -* `config.log_tags` accepts a list of methods that the `request` object responds to. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. +* `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. * `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`. @@ -161,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,13 +183,15 @@ The full set of methods that can be used in this block are as follows: * `helper` defines whether or not to generate helpers. Defaults to `true`. * `integration_tool` defines which integration tool to use. Defaults to `nil`. * `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 `nil`. +* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. +* `resource_route` defines whether a resource route definition should be generated + 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`. -* `test_framework` defines which test framework to use. Defaults to `false` and will use Test::Unit by default. +* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default. * `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`. ### Configuring Middleware @@ -199,7 +199,7 @@ The full set of methods that can be used in this block are as follows: Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: * `ActionDispatch::SSL` forces every request to be under HTTPS protocol. Will be available if `config.force_ssl` is set to `true`. Options passed to this can be configured by using `config.ssl_options`. -* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_files` is `false`. +* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_files` is `false`. Set `config.static_index` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.static_index` to `"main"`. * `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when `config.cache_classes` is `false`. * `ActiveSupport::Cache::Strategy::LocalCache` serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. * `Rack::Runtime` sets an `X-Runtime` header, containing the time (in seconds) taken to execute the request. @@ -286,7 +286,7 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default. -* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:number`. +* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`. * `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`. @@ -300,6 +300,18 @@ All these configuration options are delegated to the `I18n` library. `config/environments/production.rb` which is generated by Rails. The default value is true if this configuration is not set. +* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling db:structure:dump. + The options are `:schema_search_path` (the default) which dumps any schemas listed in schema_search_path, + `:all` which always dumps all schemas regardless of the schema_search_path, + or a string of comma separated schemas. + +* `config.active_record.belongs_to_required_by_default` is a boolean value and controls whether `belongs_to` association is required by default. + +* `config.active_record.warn_on_records_fetched_greater_than` allows setting a + warning threshold for query result size. If the number of records returned + by a query exceeds the threshold, a warning is logged. This can be used to + identify queries which might be causing memory bloat. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. @@ -410,13 +422,23 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. end ``` -* `config.action_view.default_form_builder` tells Rails which form builder to use by default. The default is `ActionView::Helpers::FormBuilder`. If you want your form builder class to be loaded after initialization (so it's reloaded on each request in development), you can pass it as a `String` +* `config.action_view.default_form_builder` tells Rails which form builder to + use by default. The default is `ActionView::Helpers::FormBuilder`. If you + want your form builder class to be loaded after initialization (so it's + reloaded on each request in development), you can pass it as a `String`. * `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging. * `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information. -* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` +* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to + set the default behavior for `authenticity_token` in forms with `remote: + true`. By default it's set to false, which means that remote forms will not + include `authenticity_token`, which is helpful when you're fragment-caching + the form. Remote forms get the authenticity from the `meta` tag, so embedding + is unnecessary unless you support browsers without JavaScript. In such case + you can either pass `authenticity_token: true` as a form option or set this + config setting to `true`. * `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: @@ -426,7 +448,8 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. -* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations +* `config.action_view.raise_on_missing_translations` determines whether an + error should be raised for missing translations. ### Configuring Action Mailer @@ -493,15 +516,18 @@ 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: * `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded. -* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:sorted` and `:random`. Currently defaults to `:sorted`. In Rails 5.0, the default will be changed to `:random` instead. +* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:random` and `:sorted`. This option is set to `:random` in `config/environments/test.rb` in newly-generated applications. If you have an application that does not specify a `test_order`, it will default to `:sorted`, *until* Rails 5.0, when the default will become `:random`. -* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `false`. +* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`. * `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`. @@ -519,6 +545,58 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. +### Configuring Active Job + +`config.active_job` provides the following configuration options: + +* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + + ```ruby + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + ``` + +* `config.active_job.default_queue_name` can be used to change the default queue name. By default this is `"default"`. + + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` + +* `config.active_job.queue_name_prefix` allows you to set an optional, non-blank, queue name prefix for all jobs. By default it is blank and not used. + + The following configuration would queue the given job on the `production_high_priority` queue when run in production: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + +* `config.active_job.queue_name_delimiter` has a default value of `'_'`. If `queue_name_prefix` is set, then `queue_name_delimiter` joins the prefix and the non-prefixed queue name. + + The following configuration would queue the provided job on the `video_server.low_priority` queue: + + ```ruby + # prefix must be set for delimiter to be used + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` + +* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. ### Configuring a Database @@ -692,7 +770,7 @@ development: pool: 5 ``` -Prepared Statements are enabled by default on PostgreSQL. You can be disable prepared statements by setting `prepared_statements` to `false`: +Prepared Statements are enabled by default on PostgreSQL. You can disable prepared statements by setting `prepared_statements` to `false`: ```yaml production: @@ -817,15 +895,6 @@ server { Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. -#### Considerations when deploying to a subdirectory - -Deploying to a subdirectory in production has implications on various parts of -Rails. - -* development environment: -* testing environment: -* serving static assets: -* asset pipeline: Rails Environment Settings -------------------------- @@ -955,6 +1024,11 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set - + to `Rails.logger`. + +* `active_job.set_configs` Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. + * `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. * `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. @@ -973,8 +1047,6 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `load_environment_config` Loads the `config/environments` file for the current environment. -* `append_asset_paths` Finds asset paths for the application and all attached railties and keeps a track of the available directories in `config.static_asset_paths`. - * `prepend_helpers_path` Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. * `load_config_initializers` Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. @@ -1049,3 +1121,23 @@ These configuration points are then available through the configuration object: Rails.configuration.x.super_debugger # => true Rails.configuration.x.super_debugger.not_set # => nil ``` + +Search Engines Indexing +----------------------- + +Sometimes, you may want to prevent some pages of your application to be visible +on search sites like Google, Bing, Yahoo or Duck Duck Go. The robots that index +these sites will first analyze the `http://your-site.com/robots.txt` file to +know which pages it is allowed to index. + +Rails creates this file for you inside the `/public` folder. By default, it allows +search engines to index all pages of your application. If you want to block +indexing on all pages of you application, use this: + +``` +User-agent: * +Disallow: / +``` + +To block just specific pages, it's necessary to use a more complex syntax. Learn +it on the [official documentation](http://www.robotstxt.org/robotstxt.html). diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index db3f19f8ac..3279c99c42 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Contributing to Ruby on Rails ============================= @@ -26,21 +26,25 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge ### Creating a Bug Report -If you've found a problem in Ruby on Rails which is not a security risk, do a search in 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.) +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. 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. -### Create a Self-Contained gist for Active Record and Action Controller Issues +### Create an Executable Test Case -If you are filing a bug report, please use -[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or -[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) -if the bug is found in a published gem, and -[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or -[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) -if the bug happens in the master branch. +Having a way to reproduce your issue will be very helpful for others to help confirm, investigate and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point: + +* Template for Active Record (models, database) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) +* Template for Action Pack (controllers, routing) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) +* Generic template for other issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) + +These templates include the boilerplate code to set up a test case against either a released version of Rails (`*_gem.rb`) or edge Rails (`*_master.rb`). + +Simply copy the content of the appropriate template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing. + +You can then share your executable test case as a [gist](https://gist.github.com), or simply paste the content into the issue description. ### Special Treatment for Security Issues @@ -62,7 +66,7 @@ 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. -If you'd like feedback on an idea for a feature before doing the work for make +If you'd like feedback on an idea for a feature before doing the work to make a patch, please send an email to the [rails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You might get no response, which means that everyone is indifferent. You might find @@ -75,17 +79,17 @@ discussions new features require. Helping to Resolve Existing Issues ---------------------------------- -As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [Everyone's Issues](https://github.com/rails/rails/issues) list in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: +As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: ### Verifying Bug Reports For starters, it helps just to verify bug reports. Can you reproduce the reported issue on your own computer? If so, you can add a comment to the issue saying that you're seeing the same thing. -If something is very vague, can you help squash it down into something specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. +If an issue is very vague, can you help narrow it down to something more specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "Contributing to the Rails Code" section. -Anything you can do to make bug reports more succinct or easier to reproduce is a help to folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. +Anything you can do to make bug reports more succinct or easier to reproduce helps folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. ### Testing Patches @@ -113,7 +117,7 @@ Once you're happy that the pull request contains a good change, comment on the G >I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. +If your comment simply reads "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. Contributing to the Rails Documentation --------------------------------------- @@ -173,6 +177,14 @@ $ git checkout -b my_new_branch It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository. +### Bundle install + +Install the required gems. + +```bash +$ bundle install +``` + ### Running an Application Against Your Local Branch In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch: @@ -236,11 +248,11 @@ This will generate a report with the following information: ``` Calculating ------------------------------------- - addition 69114 i/100ms - addition with send 64062 i/100ms + addition 132.013k i/100ms + addition with send 125.413k i/100ms ------------------------------------------------- - addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s - addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s + addition 9.677M (± 1.7%) i/s - 48.449M + addition with send 6.794M (± 1.1%) i/s - 33.987M ``` Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. @@ -347,9 +359,9 @@ $ RUBYOPT=-W0 bundle exec rake test The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -366,7 +378,12 @@ A CHANGELOG entry should summarize what was changed and should end with author's *Your Name* ``` -Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. +Your name can be added directly after the last word if there are no code +examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. + +### Updating the Gemfile.lock + +Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. ### Sanity Check @@ -386,21 +403,27 @@ When you're happy with the code on your computer, you need to commit the changes $ git commit -a ``` -At this point, your editor should be fired up and you can write a message for this commit. Well formatted and descriptive commit messages are extremely helpful for the others, especially when figuring out why given change was made, so please take the time to write it. +This should fire up your editor to write a commit message. When you have +finished, save and close to continue. + +A well-formatted and descriptive commit message is very helpful to others for +understanding why the change was made, so please take the time to write it. -Good commit message should be formatted according to the following example: +A good commit message looks like this: ``` Short summary (ideally 50 characters or less) -More detailed description, if necessary. It should be wrapped to 72 -characters. Try to be as descriptive as you can, even if you think that -the commit content is obvious, it may not be obvious to others. You -should add such description also if it's already present in bug tracker, -it should not be necessary to visit a webpage to check the history. +More detailed description, if necessary. It should be wrapped to +72 characters. Try to be as descriptive as you can. Even if you +think that the commit content is obvious, it may not be obvious +to others. Add any description that is already present in the +relevant issues; it should not be necessary to visit a webpage +to check the history. + +The description section can have multiple paragraphs. -Description can have multiple paragraphs and you can use code examples -inside, just indent it with 4 spaces: +Code examples can be embedded by indenting them with 4 spaces: class ArticlesController def index @@ -410,13 +433,15 @@ inside, just indent it with 4 spaces: You can also add bullet points: -- you can use dashes or asterisks +- make a bullet point by starting a line with either a dash (-) + or an asterisk (*) -- also, try to indent next line of a point for readability, if it's too - long to fit in 72 characters +- wrap lines at 72 characters, and indent any additional lines + with 2 spaces for readability ``` -TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. +TIP. Please squash your commits into a single commit when appropriate. This +simplifies future cherry picks and keeps the git log clean. ### Update Your Branch @@ -507,7 +532,7 @@ pull request". The Rails core team will be notified about your submission. Most pull requests will go through a few iterations before they get merged. Different contributors will sometimes have different opinions, and often -patches will need revised before they can get merged. +patches will need to be revised before they can get merged. Some contributors to Rails have email notifications from GitHub turned on, but others do not. Furthermore, (almost) everyone who works on Rails is a @@ -554,8 +579,7 @@ following: ```bash $ git fetch upstream $ git checkout my_pull_request -$ git rebase upstream/master -$ git rebase -i +$ git rebase -i upstream/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index cef9ac083b..dc1df8f229 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Debugging Rails Applications ============================ @@ -17,7 +17,7 @@ After reading this guide, you will know: View Helpers for Debugging -------------------------- -One common task is to inspect the contents of a variable. In Rails, you can do this with three methods: +One common task is to inspect the contents of a variable. Rails provides three different ways to do this: * `debug` * `to_yaml` @@ -54,7 +54,7 @@ Title: Rails debugging guide ### `to_yaml` -Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: +Alternatively, calling `to_yaml` on any object converts it to YAML. You can pass this converted object into the `simple_format` helper method to format the output. This is how `debug` does its magic. ```html+erb <%= simple_format @article.to_yaml %> @@ -64,9 +64,7 @@ Displaying an instance variable, or any other object or method, in YAML format c </p> ``` -The `to_yaml` method converts the method to YAML format leaving it more readable, and then the `simple_format` helper is used to render each line as in the console. This is how `debug` method does its magic. - -As a result of this, you will have something like this in your view: +The above code will render something like this: ```yaml --- !ruby/object Article @@ -94,7 +92,7 @@ Another useful method for displaying object values is `inspect`, especially when </p> ``` -Will be rendered as follows: +Will render: ``` [1, 2, 3, 4, 5] @@ -109,9 +107,9 @@ It can also be useful to save information to log files at runtime. Rails maintai ### What is the Logger? -Rails makes use of the `ActiveSupport::Logger` class to write log information. You can also substitute another logger such as `Log4r` if you wish. +Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted. -You can specify an alternative logger in your `environment.rb` or any environment file: +You can specify an alternative logger in `environment.rb` or any other environment file, for example: ```ruby Rails.logger = Logger.new(STDOUT) @@ -129,16 +127,21 @@ TIP: By default, each log is created under `Rails.root/log/` and the log file is ### Log Levels -When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the `Rails.logger.level` method. +When something is logged, it's printed into the corresponding log if the log +level of the message is equal to or higher than the configured log level. If you +want to know the current log level, you can call the `Rails.logger.level` +method. -The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`, corresponding to the log level numbers from 0 up to 5 respectively. To change the default log level, use +The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, +and `:unknown`, corresponding to the log level numbers from 0 up to 5, +respectively. To change the default log level, use ```ruby config.log_level = :warn # In any environment initializer, or Rails.logger.level = 0 # at any time ``` -This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information. +This is useful when you want to log under development or staging without flooding your production log with unnecessary information. TIP: The default Rails log level is `debug` in all environments. @@ -202,7 +205,7 @@ Adding extra logging like this makes it easy to search for unexpected or unusual When running multi-user, multi-account applications, it's often useful to be able to filter the logs using some custom rules. `TaggedLogging` -in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. +in Active Support helps you do exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. ```ruby logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) @@ -212,34 +215,33 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs " ``` ### Impact of Logs on Performance -Logging will always have a small impact on performance of your rails app, - particularly when logging to disk. However, there are a few subtleties: +Logging will always have a small impact on the performance of your Rails app, + particularly when logging to disk. Additionally, there are a few subtleties: Using the `:debug` level will have a greater performance penalty than `:fatal`, as a far greater number of strings are being evaluated and written to the log output (e.g. disk). -Another potential pitfall is that if you have many calls to `Logger` like this - in your code: +Another potential pitfall is too many calls to `Logger` in your code: ```ruby logger.debug "Person attributes hash: #{@person.attributes.inspect}" ``` -In the above example, There will be a performance impact even if the allowed +In the above example, there will be a performance impact even if the allowed output level doesn't include debug. The reason is that Ruby has to evaluate these strings, which includes instantiating the somewhat heavy `String` object -and interpolating the variables, and which takes time. +and interpolating the variables. Therefore, it's recommended to pass blocks to the logger methods, as these are -only evaluated if the output level is the same or included in the allowed level +only evaluated if the output level is the same as — or included in — the allowed level (i.e. lazy loading). The same code rewritten would be: ```ruby logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` -The contents of the block, and therefore the string interpolation, is only -evaluated if debug is enabled. This performance savings is only really +The contents of the block, and therefore the string interpolation, are only +evaluated if debug is enabled. This performance savings are only really noticeable with large amounts of logging, but it's a good practice to employ. Debugging with the `byebug` gem @@ -253,8 +255,7 @@ is your best companion. The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and -use this guide to learn how to move from the code you have written deeper into -Rails code. +use this guide to learn how to move from the code you have written into the underlying Rails code. ### Setup @@ -285,7 +286,7 @@ As soon as your application calls the `byebug` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(byebug)`. Before the prompt, the code around the line that is about to be run will be -displayed and the current line will be marked by '=>'. Like this: +displayed and the current line will be marked by '=>', like this: ``` [1, 10] in /PathTo/project/app/controllers/articles_controller.rb @@ -316,7 +317,7 @@ For example: => Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) => Ctrl-C to shutdown server [2014-04-11 13:11:47] INFO WEBrick 1.3.1 -[2014-04-11 13:11:47] INFO ruby 2.1.1 (2014-02-24) [i686-linux] +[2014-04-11 13:11:47] INFO ruby 2.2.2 (2015-04-13) [i686-linux] [2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000 @@ -339,7 +340,7 @@ Processing by ArticlesController#index as HTML (byebug) ``` -Now it's time to explore and dig into your application. A good place to start is +Now it's time to explore your application. A good place to start is by asking the debugger for help. Type: `help` ``` @@ -360,9 +361,9 @@ 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, so you can also use `l` for the `list` command, for example. +commands. For example, you can use `l` for the `list` command. -To see the previous ten lines you should type `list-` (or `l-`) +To see the previous ten lines you should type `list-` (or `l-`). ``` (byebug) l- @@ -381,7 +382,7 @@ To see the previous ten lines you should type `list-` (or `l-`) ``` -This way you can move inside the file, being able to see the code above and over +This way you can move inside the file and see the code above the line where you added the `byebug` call. Finally, to see where you are in the code again you can type `list=` @@ -411,8 +412,7 @@ contexts as you go through the different parts of the stack. The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables the debugger to inspect the frame stack, evaluate variables from the perspective of the -debugged program, and contains information about the place where the debugged -program is stopped. +debugged program, and know the place where the debugged program is stopped. At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got @@ -476,9 +476,8 @@ character and the number indicates the current thread of execution. * `thread resume _n_` resumes thread _n_. * `thread switch _n_` switches the current thread context to _n_. -This command is very helpful, among other occasions, when you are debugging -concurrent threads and need to verify that there are no race conditions in your -code. +This command is very helpful when you are debugging concurrent threads and need +to verify that there are no race conditions in your code. ### Inspecting Variables @@ -532,19 +531,22 @@ 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, :@_env, :@_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 was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). -This way an irb session will be started within the context you invoked it. But +This will start an irb session within the context you invoked it. But be warned: this is an experimental feature. The `var` method is the most convenient way to show variables and their values. -Let's let `byebug` to help us with it. +Let's have `byebug` help us with it. ``` (byebug) help var @@ -556,7 +558,7 @@ v[ar] l[ocal] show local variables ``` This is a great way to inspect the values of the current context variables. For -example, to check that we have no local variables currently defined. +example, to check that we have no local variables currently defined: ``` (byebug) var local @@ -587,14 +589,14 @@ tracking the values of a variable while the execution goes on. 1: @articles = nil ``` -The variables inside the displaying list will be printed with their values after +The variables inside the displayed list will be printed with their values after you move in the stack. To stop displaying a variable use `undisplay _n_` where _n_ is the variable number (1 in the last example). ### Step by Step Now you should know where you are in the running trace and be able to print the -available variables. But lets continue and move on with the application +available variables. But let's continue and move on with the application execution. Use `step` (abbreviated `s`) to continue running your program until the next @@ -651,8 +653,8 @@ Next went up a frame because previous frame finished (byebug) ``` -If we use `step` in the same situation, we will literally go the next ruby -instruction to be executed. In this case, the activesupport's `week` method. +If we use `step` in the same situation, we will literally go to the next Ruby +instruction to be executed. In this case, Active Support's `week` method. ``` (byebug) step @@ -672,8 +674,7 @@ instruction to be executed. In this case, the activesupport's `week` method. (byebug) ``` -This is one of the best ways to find bugs in your code, or perhaps in Ruby on -Rails. +This is one of the best ways to find bugs in your code. ### Breakpoints @@ -780,12 +781,12 @@ will be stopped and you will have to start it again. ### Settings -`byebug` has a few available options to tweak its behaviour: +`byebug` has a few available options to tweak its behavior: -* `set autoreload`: Reload source code when changed (default: true). -* `set autolist`: Execute `list` command on every breakpoint (default: true). +* `set autoreload`: Reload source code when changed (defaults: true). +* `set autolist`: Execute `list` command on every breakpoint (defaults: true). * `set listsize _n_`: Set number of source lines to list by default to _n_ -(default: 10) +(defaults: 10) * `set forcestep`: Make sure the `next` and `step` commands always move to a new line. @@ -800,10 +801,67 @@ set forcestep set listsize 25 ``` +Debugging with the `web-console` gem +------------------------------------ + +Web Console is a bit like `byebug`, but it runs in the browser. In any page you +are developing, you can request a console in the context of a view or a +controller. The console would be rendered next to your HTML content. + +### Console + +Inside any controller action or view, you can invoke the console by +calling the `console` method. + +For example, in a controller: + +```ruby +class PostsController < ApplicationController + def new + console + @post = Post.new + end +end +``` + +Or in a view: + +```html+erb +<% console %> + +<h2>New Post</h2> +``` + +This will render a console inside your view. You don't need to care about the +location of the `console` call; it won't be rendered on the spot of its +invocation but next to your HTML content. + +The console executes pure Ruby code: You can define and instantiate +custom classes, create new models and inspect variables. + +NOTE: Only one console can be rendered per request. Otherwise `web-console` +will raise an error on the second `console` invocation. + +### Inspecting Variables + +You can invoke `instance_variables` to list all the instance variables +available in your context. If you want to list all the local variables, you can +do that with `local_variables`. + +### Settings + +* `config.web_console.whitelisted_ips`: Authorized list of IPv4 or IPv6 +addresses and networks (defaults: `127.0.0.1/8, ::1`). +* `config.web_console.whiny_requests`: Log a message when a console rendering +is prevented (defaults: `true`). + +Since `web-console` evaluates plain Ruby code remotely on the server, don't try +to use it in production. + Debugging Memory Leaks ---------------------- -A Ruby application (on Rails or not), can leak memory - either in the Ruby code +A Ruby application (on Rails or not), can leak memory — either in the Ruby code or at the C code level. In this section, you will learn how to find and fix such leaks by using tool @@ -832,9 +890,9 @@ application. Here is a list of useful plugins for debugging: * [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has footnotes that give request information and link back to your source via TextMate. -* [Query Trace](https://github.com/ntalbott/query_trace/tree/master) Adds query +* [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query origin tracing to your logs. -* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin +* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed. @@ -846,7 +904,7 @@ standard Rails error page with a new one containing more contextual information, like source code and variable inspection. * [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails development that will end your tailing of development.log. Have all information -about your Rails app requests in the browser - in the Developer Tools panel. +about your Rails app requests in the browser — in the Developer Tools panel. Provides insight to db/rendering/total times, parameter list, rendered views and more. @@ -856,6 +914,7 @@ References * [ruby-debug Homepage](http://bashdb.sourceforge.net/ruby-debug/home-page.html) * [debugger Homepage](https://github.com/cldwalker/debugger) * [byebug Homepage](https://github.com/deivid-rodriguez/byebug) +* [web-console Homepage](https://github.com/rails/web-console) * [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/debug-rails-app-ruby-debug/) * [Ryan Bates' debugging ruby (revised) screencast](http://railscasts.com/episodes/54-debugging-ruby-revised) * [Ryan Bates' stack trace screencast](http://railscasts.com/episodes/24-the-stack-trace) diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 9eacc3a2fe..295e48f493 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Development Dependencies Install ================================ @@ -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. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 67032a31f5..9145aee009 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 @@ -33,7 +33,7 @@ url: active_record_querying.html description: This guide covers the database query interface provided by Active Record. - - name: Active Model basics + name: Active Model Basics url: active_model_basics.html description: This guide covers the use of model classes without Active Record. work_in_progress: true @@ -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,7 +116,7 @@ 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 diff --git a/guides/source/engines.md b/guides/source/engines.md index 731178787f..3b1588b75a 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Getting Started with Engines ============================ @@ -368,7 +368,7 @@ called `Blorgh::ArticlesController` (at `app/controllers/blorgh/articles_controller.rb`) and its related views at `app/views/blorgh/articles`. This generator also generates a test for the controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper -(`app/helpers/blorgh/articles_controller.rb`). +(`app/helpers/blorgh/articles_helper.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: @@ -402,15 +402,6 @@ Finally, the assets for this resource are generated in two files: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `<head>` -tag of this layout: - -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open @@ -591,7 +582,7 @@ the comments, however, is not quite right yet. If you were to create a comment right now, you would see this error: ``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * "/Users/ryan/Sites/side_projects/blorgh/app/views" @@ -600,7 +591,7 @@ Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw -this error. The engine knows to look for `blorgh/comments/comment` because the +this error. The engine knows to look for `blorgh/comments/_comment` because the model object it is receiving is from the `Blorgh::Comment` class. This partial will be responsible for rendering just the comment text, for now. @@ -698,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 @@ -831,11 +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: @@ -890,7 +879,9 @@ engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like: ```ruby -class Blorgh::ApplicationController < ApplicationController +module Blorgh + class ApplicationController < ::ApplicationController + end end ``` @@ -1199,7 +1190,7 @@ end ``` ```ruby -# Blorgh/lib/concerns/models/article +# Blorgh/lib/concerns/models/article.rb module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index a8dcd3ee4f..8f7d97844e 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Form Helpers ============ @@ -241,7 +241,7 @@ Upon form submission the value entered by the user will be stored in `params[:pe WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object. -Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](./active_record_validations.html#displaying-validation-errors-in-views) guide. +Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](active_record_validations.html#displaying-validation-errors-in-views) guide. ### Binding a Form to an Object @@ -275,14 +275,14 @@ There are a few things to note here: The resulting HTML is: ```html -<form accept-charset="UTF-8" action="/articles/create" method="post" class="nifty_form"> +<form accept-charset="UTF-8" action="/articles" method="post" class="nifty_form"> <input id="article_title" name="article[title]" type="text" /> <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea> <input name="commit" type="submit" value="Create" /> </form> ``` -The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the parameter_names section. +The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions). The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. @@ -291,8 +291,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 %> ``` @@ -300,7 +300,7 @@ You can create a similar binding without actually creating `<form>` tags with th which produces the following output: ```html -<form accept-charset="UTF-8" action="/people/create" class="new_person" id="new_person" method="post"> +<form accept-charset="UTF-8" action="/people" class="new_person" id="new_person" method="post"> <input id="person_name" name="person[name]" type="text" /> <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" /> </form> @@ -441,7 +441,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: @@ -687,7 +687,14 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder end ``` -If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option. +If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option: + +```ruby +def labeled_form_for(record, options = {}, &block) + options.merge! builder: LabellingFormBuilder + form_for record, options, &block +end +``` The form builder used also determines what happens when you do @@ -722,7 +729,7 @@ The two basic structures are arrays and hashes. Hashes mirror the syntax used fo the `params` hash will contain -```erb +```ruby {'person' => {'name' => 'Henry'}} ``` diff --git a/guides/source/generators.md b/guides/source/generators.md index 05bf07b4c8..14f451cbc9 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Creating and Customizing Rails Generators & Templates ===================================================== diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index d80622ef00..e64a788ac2 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Getting Started with Rails ========================== @@ -23,7 +23,7 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer. +* 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). * A working installation of the [SQLite3 Database](https://www.sqlite.org). @@ -50,7 +50,7 @@ code while accomplishing more than many other languages and frameworks. Experienced Rails developers also report that it makes web application development more fun. -Rails is opinionated software. It makes the assumption that there is the "best" +Rails is opinionated software. It makes the assumption that there is a "best" way to do things, and it's designed to encourage that way - and in some cases to discourage alternatives. If you learn "The Rails Way" you'll probably discover a tremendous increase in productivity. If you persist in bringing old habits from @@ -97,7 +97,7 @@ For more installation methods for most Operating Systems take a look at ```bash $ ruby -v -ruby 2.0.0p353 +ruby 2.2.2p95 ``` Many popular UNIX-like OSes ship with an acceptable version of SQLite3. @@ -193,6 +193,9 @@ following in the `blog` directory: $ bin/rails server ``` +TIP: If you are using Windows, you have to pass the scripts under the `bin` +folder directly to the Ruby interpreter e.g. `ruby bin\rails server`. + TIP: Compiling CoffeeScript and JavaScript asset compression requires you have a JavaScript runtime available on your system, in the absence of a runtime you will see an `execjs` error during asset compilation. @@ -201,7 +204,7 @@ Rails adds the `therubyracer` gem to the generated `Gemfile` in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to the `Gemfile` in apps generated under JRuby. You can investigate -all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). +all the supported runtimes at [ExecJS](https://github.com/rails/execjs#readme). This will fire up WEBrick, a web server distributed with Ruby by default. To see your application in action, open a browser window and navigate to @@ -318,9 +321,9 @@ root 'welcome#index' application to the welcome controller's index action and `get 'welcome/index'` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the -controller generator (`rails generate controller welcome index`). +controller generator (`bin/rails generate controller welcome index`). -Launch the web server again if you stopped it to generate the controller (`rails +Launch the web server again if you stopped it to generate the controller (`bin/rails server`) and navigate to <http://localhost:3000> in your browser. You'll see the "Hello, Rails!" message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` @@ -353,7 +356,7 @@ Rails.application.routes.draw do end ``` -If you run `rake routes`, you'll see that it has defined routes for all the +If you run `bin/rake routes`, you'll see that it has defined routes for all the standard RESTful actions. The meaning of the prefix column (and other columns) will be seen later, but for now notice that Rails has inferred the singular form `article` and makes meaningful use of the distinction. @@ -374,7 +377,7 @@ edit_article GET /articles/:id/edit(.:format) articles#edit In the next section, you will add the ability to create new articles in your application and be able to view them. This is the "C" and the "R" from CRUD: -creation and reading. The form for doing this will look like this: +create and read. The form for doing this will look like this: ![The new article form](images/getting_started/new_article.png) @@ -397,7 +400,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` @@ -553,7 +556,7 @@ this: In this example, the `articles_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of -`rake routes`: +`bin/rake routes`: ```bash $ bin/rake routes @@ -663,7 +666,7 @@ models, as that will be done automatically by Active Record. ### Running a Migration -As we've just seen, `rails generate model` created a _database migration_ file +As we've just seen, `bin/rails generate model` created a _database migration_ file inside the `db/migrate` directory. Migrations are Ruby classes that are designed to make it simple to create and modify database tables. Rails uses rake commands to run migrations, and it's possible to undo a migration after @@ -716,7 +719,7 @@ NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the `development` section of your `config/database.yml` file. If you would like to execute migrations in another environment, for instance in production, you must explicitly pass it when -invoking the command: `rake db:migrate RAILS_ENV=production`. +invoking the command: `bin/rake db:migrate RAILS_ENV=production`. ### Saving data in the controller @@ -803,7 +806,7 @@ If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the `show` action before proceeding. -As we have seen in the output of `rake routes`, the route for `show` action is +As we have seen in the output of `bin/rake routes`, the route for `show` action is as follows: ``` @@ -833,7 +836,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` A couple of things to note. We use `Article.find` to find the article we're @@ -865,7 +868,7 @@ Visit <http://localhost:3000/articles/new> and give it a try! ### Listing all articles We still need a way to list all our articles, so let's do that. -The route for this as per output of `rake routes` is: +The route for this as per output of `bin/rake routes` is: ``` articles GET /articles(.:format) articles#index @@ -889,7 +892,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` And then finally, add the view for this action, located at @@ -908,6 +911,7 @@ And then finally, add the view for this action, located at <tr> <td><%= article.title %></td> <td><%= article.text %></td> + <td><%= link_to 'Show', article_path(article) %></td> </tr> <% end %> </table> @@ -1236,10 +1240,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: You don't need to pass all attributes to `update`. For -example, if you'd call `@article.update(title: 'A new title')` -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 @@ -1271,8 +1274,8 @@ bottom of the template: ```html+erb ... -<%= link_to 'Back', articles_path %> | -<%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` And here's how our app looks so far: @@ -1359,7 +1362,7 @@ Then do the same for the `app/views/articles/edit.html.erb` view: We're now ready to cover the "D" part of CRUD, deleting articles from the database. Following the REST convention, the route for -deleting articles as per output of `rake routes` is: +deleting articles as per output of `bin/rake routes` is: ```ruby DELETE /articles/:id(.:format) articles#destroy @@ -1475,16 +1478,20 @@ Finally, add a 'Destroy' link to your `index` action template ``` Here we're using `link_to` in a different way. We pass the named route as the -second argument, and then the options as another argument. The `:method` and -`:'data-confirm'` options are used as HTML5 attributes so that when the link is -clicked, Rails will first show a confirm dialog to the user, and then submit the -link with method `delete`. This is done via the JavaScript file `jquery_ujs` -which is automatically included into your application's layout -(`app/views/layouts/application.html.erb`) when you generated the application. -Without this file, the confirmation dialog box wouldn't appear. +second argument, and then the options as another argument. The `method: :delete` +and `data: { confirm: 'Are you sure?' }` options are used as HTML5 attributes so +that when the link is clicked, Rails will first show a confirm dialog to the +user, and then submit the link with method `delete`. This is done via the +JavaScript file `jquery_ujs` which is automatically included in your +application's layout (`app/views/layouts/application.html.erb`) when you +generated the application. Without this file, the confirmation dialog box won't +appear. ![Confirm Dialog](images/getting_started/confirm_dialog.png) +TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on +[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide. + Congratulations, you can now create, show, list, update and destroy articles. @@ -1502,7 +1509,7 @@ comments on articles. We're going to see the same generator that we used before when creating the `Article` model. This time we'll create a `Comment` model to hold -reference of article comments. Run this command in your terminal: +reference to an article. Run this command in your terminal: ```bash $ bin/rails generate model Comment commenter:string body:text article:references @@ -1514,7 +1521,7 @@ This command will generate four files: | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | | app/models/comment.rb | The Comment model | -| test/models/comment_test.rb | Testing harness for the comments model | +| test/models/comment_test.rb | Testing harness for the comment model | | test/fixtures/comments.yml | Sample comments for use in testing | First, take a look at `app/models/comment.rb`: @@ -1681,8 +1688,8 @@ So first, we'll wire up the Article show template </p> <% end %> -<%= link_to 'Back', articles_path %> | -<%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` This adds a form on the `Article` show page that creates a new comment by @@ -1762,8 +1769,8 @@ add that to the `app/views/articles/show.html.erb`. </p> <% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` Now you can add articles and comments to your blog and have them show up in the @@ -1828,8 +1835,8 @@ following: </p> <% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` This will now render the partial in `app/views/comments/_comment.html.erb` once @@ -1878,8 +1885,8 @@ Then you make the `app/views/articles/show.html.erb` look like the following: <h2>Add a comment:</h2> <%= render 'comments/form' %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` The second render just defines the partial template we want to render, @@ -1995,7 +2002,7 @@ class ArticlesController < ApplicationController @articles = Article.all end - # snipped for brevity + # snippet for brevity ``` We also want to allow only authenticated users to delete comments, so in the @@ -2011,7 +2018,7 @@ class CommentsController < ApplicationController # ... end - # snipped for brevity + # snippet for brevity ``` Now if you try to create a new article, you will be greeted with a basic HTTP @@ -2048,19 +2055,6 @@ resources: * The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk) * The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net -Rails also comes with built-in help that you can generate using the rake -command-line utility: - -* Running `rake doc:guides` will put a full copy of the Rails Guides in the - `doc/guides` folder of your application. Open `doc/guides/index.html` in your - web browser to explore the Guides. -* Running `rake doc:rails` will put a full copy of the API documentation for - Rails in the `doc/api` folder of your application. Open `doc/api/index.html` - in your web browser to explore the API documentation. - -TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake -task you need to install the Redcarpet and Nokogiri gems. Add it to your `Gemfile` and run -`bundle install` and you're ready to go. Configuration Gotchas --------------------- diff --git a/guides/source/i18n.md b/guides/source/i18n.md index bd6babff41..9f0ed1a85b 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Rails Internationalization (I18n) API ===================================== @@ -40,7 +40,7 @@ Internationalization is a complex problem. Natural languages differ in so many w * providing support for English and similar languages out of the box * making it easy to customize and extend everything for other languages -As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults. +As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. ### The Overall Architecture of the Library @@ -84,13 +84,13 @@ So, let's internationalize a simple Rails application from the ground up in the Setup the Rails Application for Internationalization ---------------------------------------------------- -There are just a few simple steps to get up and running with I18n support for your application. +There are a few steps to get up and running with I18n support for a Rails application. ### Configure the I18n Module -Following the _convention over configuration_ philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily. +Following the _convention over configuration_ philosophy, Rails I18n provides reasonable default translation strings. When different translation strings are needed, they can be overridden. -Rails adds all `.rb` and `.yml` files from the `config/locales` directory to your **translations load path**, automatically. +Rails adds all `.rb` and `.yml` files from the `config/locales` directory to the **translations load path**, automatically. The default `en.yml` locale in this directory contains a sample pair of translation strings: @@ -101,15 +101,15 @@ en: This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend. -The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. +The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations. NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. -The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. +The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme. -NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. +NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced. -The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -117,31 +117,25 @@ The default `application.rb` file has instructions on how to add locales from an # config.i18n.default_locale = :de ``` -### Optional: Custom I18n Configuration Setup - -For the sake of completeness, let's mention that if you do not want to use the `application.rb` file for some reason, you can always wire up things manually, too. - -To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an initializer: +The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `application.rb`: ```ruby -# in config/initializers/locale.rb +# config/initializers/locale.rb -# tell the I18n library where to find your translations +# Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# set default locale to something other than :en +# Set default locale to something other than :en I18n.default_locale = :pt ``` -### Setting and Passing the Locale - -If you want to translate your Rails application to a **single language other than English** (the default locale), you can set I18n.default_locale to your locale in `application.rb` or an initializer as shown above, and it will persist through the requests. +### Managing the Locale across Requests -However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. +The default locale is used for all translations unless `I18n.locale` is explicitly set. -WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. +A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. -The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: +The locale can be set in a `before_action` in the `ApplicationController`: ```ruby before_action :set_locale @@ -151,11 +145,11 @@ def set_locale end ``` -This requires you to pass the locale as a URL query parameter as in `http://example.com/books?locale=pt`. (This is, for example, Google's approach.) So `http://localhost:3000?locale=pt` will load the Portuguese localization, whereas `http://localhost:3000?locale=de` would load the German localization, and so on. You may skip the next section and head over to the **Internationalize your application** section, if you want to try things out by manually placing the locale in the URL and reloading the page. +This example illustrates this using a URL query parameter to set the locale (e.g. `http://example.com/books?locale=pt`). With this approach, `http://localhost:3000?locale=pt` renders the Portuguese localization, while `http://localhost:3000?locale=de` loads a German localization. -Of course, you probably don't want to manually include the locale in every URL all over your application, or want the URLs look differently, e.g. the usual `http://example.com/pt/books` versus `http://example.com/en/books`. Let's discuss the different options you have. +The locale can be set using one of many different approaches. -### Setting the Locale from the Domain Name +#### Setting the Locale from the Domain Name One option you have is to set the locale from the domain name where your application runs. For example, we want `www.example.com` to load the English (or default) locale, and `www.example.es` to load the Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages: @@ -201,14 +195,14 @@ end If your application includes a locale switching menu, you would then have something like this in it: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `http://www.application.de`. This solution has aforementioned advantages, however, you may not be able or may not want to provide different localizations ("language versions") on different domains. The most obvious solution would be to include locale code in the URL params (or request path). -### Setting the Locale from the URL Params +#### Setting the Locale from URL Params The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. @@ -266,14 +260,23 @@ Do take special care about the **order of your routes**, so this route declarati NOTE: Have a look at various gems which simplify working with routes: [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master), [rails-translate-routes](https://github.com/francesc/rails-translate-routes), [route_translator](https://github.com/enriclluelles/route_translator). -### Setting the Locale from the Client Supplied Information +#### Setting the Locale from User Preferences -In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above. +An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user. +```ruby +def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale +end +``` + +#### Choosing an Implied Locale -#### Using `Accept-Language` +When an explicit locale has not been set for a request (e.g. via one of the above methods), an application should attempt to infer the desired locale. -One source of client supplied information would be an `Accept-Language` HTTP header. People may [set this in their browser](http://www.w3.org/International/questions/qa-lang-priorities) or other clients (such as _curl_). +##### Inferring Locale from the Language Header + +The `Accept-Language` HTTP header indicates the preferred language for request's response. Browsers [set this header value based on the user's language preference settings](http://www.w3.org/International/questions/qa-lang-priorities), making it a good first choice when inferring a locale. A trivial implementation of using an `Accept-Language` header would be: @@ -290,24 +293,27 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a gem such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). -#### Using GeoIP (or Similar) Database +In practice, more robust code is necessary to do this reliably. Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) library or Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb) Rack middleware provide solutions to this problem. + +##### Inferring the Locale from IP Geolocation -Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned. +The IP address of the client making the request can be used to infer the client's region and thus their locale. Services such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry) or gems like [geocoder](https://github.com/alexreisner/geocoder) can be used to implement this approach. -#### User Profile +In general, this approach is far less reliable than using the language header and is not recommended for most web applications. -You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value. +#### Storing the Locale from the Session or Cookies -Internationalizing your Application +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. + +Internationalization and Localization ----------------------------------- -OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff. +OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. -Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide necessary translations for these abstracts. +Next we need to _internationalize_ our application by abstracting every locale-specific element. Finally, we need to _localize_ it by providing necessary translations for these abstracts. -You most probably have something like this in one of your applications: +Given the following example: ```ruby # config/routes.rb @@ -344,9 +350,9 @@ end ![rails i18n demo untranslated](images/i18n/demo_untranslated.png) -### Adding Translations +### Abstracting Localized Code -Obviously there are **two strings that are localized to English**. In order to internationalize this code, **replace these strings** with calls to Rails' `#t` helper with a key that makes sense for the translation: +There are two strings in our code that are in English and that users will be rendered in our response ("Hello Flash" and "Hello World"). In order to internationalize this code, these strings need to be replaced by calls to Rails' `#t` helper with an appropriate key for each string: ```ruby # app/controllers/home_controller.rb @@ -363,13 +369,15 @@ end <p><%= flash[:notice] %></p> ``` -When you now render this view, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. +Now, when this view is rendered, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. ![rails i18n demo translation missing](images/i18n/demo_translation_missing.png) NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do not need to spell out `I18n.t` all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a `<span class="translation_missing">`. -So let's add the missing translations into the dictionary files (i.e. do the "localization" part): +### Providing Translations for Internationalized Strings + +Add the missing translations into the translation dictionary files: ```yaml # config/locales/en.yml @@ -383,11 +391,11 @@ pirate: hello_flash: Ahoy Flash ``` -There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows: +Because the `default_locale` hasn't changed, translations use the `:en` locale and the response renders the english strings: ![rails i18n demo translated to English](images/i18n/demo_translated_en.png) -And when you change the URL to pass the pirate locale (`http://localhost:3000?locale=pirate`), you'll get: +If the locale is set via the URL to the pirate locale (`http://localhost:3000?locale=pirate`), the response renders the pirate strings: ![rails i18n demo translated to pirate](images/i18n/demo_translated_pirate.png) @@ -395,21 +403,64 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) -### Passing variables to translations +### Passing Variables to Translations -You can use variables in the translation messages and pass their values from the view. +One key consideration for successfully internationalizing an application is to +avoid making incorrect assumptions about grammar rules when abstracting localized +code. Grammar rules that seem fundamental in one locale may not hold true in +another one. + +Improper abstraction is shown in the following example, where assumptions are +made about the ordering of the different parts of the translation. Note that Rails +provides a `number_to_currency` helper to handle the following case. ```erb -# app/views/home/index.html.erb -<%=t 'greet_username', user: "Bill", message: "Goodbye" %> +# app/views/products/show.html.erb +<%= "#{t('currency')}#{@product.price}" %> +``` + +```yaml +# config/locales/en.yml +en: + currency: "$" + +# config/locales/es.yml +es: + currency: "€" +``` + +If the product's price is 10 then the proper translation for Spanish is "10 €" +instead of "€10" but the abstraction cannot give it. + +To create proper abstraction, the I18n gem ships with a feature called variable +interpolation that allows you to use variables in translation definitions and +pass the values for these variables to the translation method. + +Proper abstraction is shown in the following example: + +```erb +# app/views/products/show.html.erb +<%= t('product_price', price: @product.price) %> ``` ```yaml # config/locales/en.yml en: - greet_username: "%{message}, %{user}!" + product_price: "$%{price}" + +# config/locales/es.yml +es: + product_price: "%{price} €" ``` +All grammatical and punctuation decisions are made in the definition itself, so +the abstraction can give a proper translation. + +NOTE: The `default` and `scope` keywords are reserved and can't be used as +variable names. If used, an `I18n::ReservedInterpolationKey` exception is raised. +If a translation expects an interpolation variable, but this has not been passed +to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. + ### Adding Date/Time Formats OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used. @@ -449,7 +500,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: @@ -489,7 +543,9 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i Overview of the I18n API Features --------------------------------- -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +You should have a good understanding of using the i18n library now and know how +to internationalize a basic Rails application. In the following chapters, we'll +cover its features in more depth. These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). @@ -530,7 +586,7 @@ Thus the following calls are equivalent: ```ruby I18n.t 'activerecord.errors.messages.record_invalid' -I18n.t 'errors.messages.record_invalid', scope: :active_record +I18n.t 'errors.messages.record_invalid', scope: :activerecord I18n.t :record_invalid, scope: 'activerecord.errors.messages' I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] ``` @@ -588,20 +644,26 @@ you can look up the `books.index.title` value **inside** `app/views/books/index. NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method. -### Interpolation +"Lazy" lookup can also be used in controllers: -In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature. +```yaml +en: + books: + create: + success: Book created! +``` -All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation: +This is useful for setting flash messages for instance: ```ruby -I18n.backend.store_translations :en, thanks: 'Thanks %{name}!' -I18n.translate :thanks, name: 'Jeremy' -# => 'Thanks Jeremy!' +class BooksController < ApplicationController + def create + # ... + redirect_to books_url, notice: t('.success') + end +end ``` -If a translation uses `:default` or `:scope` as an interpolation variable, an `I18n::ReservedInterpolationKey` exception is raised. If a translation expects an interpolation variable, but this has not been passed to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. - ### Pluralization In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. @@ -687,7 +749,7 @@ you can safely pass the username as set by the user: ```erb <%# This is safe, it is going to be escaped if needed. %> -<%= t('welcome_html', username: @current_user.username %> +<%= t('welcome_html', username: @current_user.username) %> ``` Safe strings on the other hand are interpolated verbatim. @@ -809,7 +871,7 @@ So, for example, instead of the default error message `"cannot be blank"` you co | validation | with option | message | interpolation | | ------------ | ------------------------- | ------------------------- | ------------- | -| confirmation | - | :confirmation | - | +| confirmation | - | :confirmation | attribute | | acceptance | - | :accepted | - | | presence | - | :blank | - | | absence | - | :present | - | @@ -829,6 +891,7 @@ So, for example, instead of the default error message `"cannot be blank"` you co | numericality | :equal_to | :equal_to | count | | numericality | :less_than | :less_than | count | | numericality | :less_than_or_equal_to | :less_than_or_equal_to | count | +| numericality | :other_than | :other_than | count | | numericality | :only_integer | :not_an_integer | - | | numericality | :odd | :odd | - | | numericality | :even | :even | - | @@ -1009,7 +1072,7 @@ In other contexts you might want to change this behavior, though. E.g. the defau module I18n class JustRaiseExceptionHandler < ExceptionHandler def call(exception, locale, key, options) - if exception.is_a?(MissingTranslation) + if exception.is_a?(MissingTranslationData) raise exception.to_exception else super @@ -1026,7 +1089,7 @@ This would re-raise only the `MissingTranslationData` exception, passing all oth However, if you are using `I18n::Backend::Pluralization` this handler will also raise `I18n::MissingTranslationData: translation missing: en.i18n.plural.rule` exception that should normally be ignored to fall back to the default pluralization rule for English locale. To avoid this you may use additional check for translation key: ```ruby -if exception.is_a?(MissingTranslation) && key.to_s != 'i18n.plural.rule' +if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule' raise exception.to_exception else super @@ -1072,11 +1135,8 @@ Resources Authors ------- -* [Sven Fuchs](http://www.workingwithrails.com/person/9963-sven-fuchs) (initial author) -* [Karel Minařík](http://www.workingwithrails.com/person/7476-karel-mina-k) - -If you found this guide useful, please consider recommending its authors on [workingwithrails](http://www.workingwithrails.com). - +* [Sven Fuchs](http://svenfuchs.com) (initial author) +* [Karel Minařík](http://www.karmi.cz) Footnotes --------- diff --git a/guides/source/initialization.md b/guides/source/initialization.md index a93ceb7fb5..43083ebb86 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** The Rails Initialization Process ================================ @@ -34,7 +34,7 @@ Launch! Let's start to boot and initialize the app. A Rails application is usually started by running `rails console` or `rails server`. -### `railties/bin/rails` +### `railties/exe/rails` The `rails` in the command `rails server` is a ruby executable in your load path. This executable contains the following lines: @@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the following code: ```ruby @@ -53,11 +53,11 @@ require "rails/cli" ``` The file `railties/lib/rails/cli` in turn calls -`Rails::AppRailsLoader.exec_app_rails`. +`Rails::AppLoader.exec_app`. -### `railties/lib/rails/app_rails_loader.rb` +### `railties/lib/rails/app_loader.rb` -The primary goal of the function `exec_app_rails` is to execute your app's +The primary goal of the function `exec_app` is to execute your app's `bin/rails`. If the current directory does not have a `bin/rails`, it will navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a `rails` command from anywhere inside a rails application. @@ -106,6 +106,7 @@ A standard Rails application depends on several gems, specifically: * activemodel * activerecord * activesupport +* activejob * arel * builder * bundler @@ -163,7 +164,7 @@ throwing an error message. If the command is valid, a method of the same name is called. ```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) def run_command!(command) command = parse_command(command) @@ -532,6 +533,7 @@ require "rails" action_controller action_view action_mailer + active_job rails/test_unit sprockets ).each do |framework| @@ -555,9 +557,8 @@ I18n and Rails configuration are all being defined here. The rest of `config/application.rb` defines the configuration for the `Rails::Application` which will be used once the application is fully initialized. When `config/application.rb` has finished loading Rails and defined -the application namespace, we go back to `config/environment.rb`, -where the application is initialized. For example, if the application was called -`Blog`, here we would find `Rails.application.initialize!`, which is +the application namespace, we go back to `config/environment.rb`. Here, the +application is initialized with `Rails.application.initialize!`, which is defined in `rails/application.rb`. ### `railties/lib/rails/application.rb` 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/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index c3cde49630..94cd7297e2 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Layouts and Rendering in Rails ============================== @@ -103,34 +103,6 @@ In most cases, the `ActionController::Base#render` method does the heavy lifting TIP: If you want to see the exact results of a call to `render` without needing to inspect it in a browser, you can call `render_to_string`. This method takes exactly the same options as `render`, but it returns a string instead of sending a response back to the browser. -#### Rendering Nothing - -Perhaps the simplest thing you can do with `render` is to render nothing at all: - -```ruby -render nothing: true -``` - -If you look at the response for this using cURL, you will see the following: - -```bash -$ curl -i 127.0.0.1:3000/books -HTTP/1.1 200 OK -Connection: close -Date: Sun, 24 Jan 2010 09:25:18 GMT -Transfer-Encoding: chunked -Content-Type: */*; charset=utf-8 -X-Runtime: 0.014297 -Set-Cookie: _blog_session=...snip...; path=/; HttpOnly -Cache-Control: no-cache - -$ -``` - -We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed. - -TIP: You should probably be using the `head` method, discussed later in this guide, instead of `render :nothing`. This provides additional flexibility and makes it explicit that you're only generating HTTP headers. - #### Rendering an Action's View If you want to render the view that corresponds to a different template within the same controller, you can use `render` with the name of the view: @@ -255,7 +227,7 @@ extension for the layout file. #### Rendering HTML -You can send a HTML string back to the browser by using the `:html` option to +You can send an HTML string back to the browser by using the `:html` option to `render`: ```ruby @@ -316,12 +288,13 @@ NOTE: Unless overridden, your response returned from this render option will be #### Options for `render` -Calls to the `render` method generally accept four options: +Calls to the `render` method generally accept five options: * `:content_type` * `:layout` * `:location` * `:status` +* `:formats` ##### The `:content_type` Option @@ -427,9 +400,19 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 510 | :not_extended | | | 511 | :network_authentication_required | -NOTE: If you try to render content along with a non-content status code +NOTE: If you try to render content along with a non-content status code (100-199, 204, 205 or 304), it will be dropped from the response. +##### The `:formats` Option + +Rails uses the format specified in the request (or `:html` by default). You can +change this passing the `:formats` option with a symbol or an array: + +```ruby +render formats: :xml +render formats: [:json, :xml] +``` + #### Finding Layouts To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions. @@ -553,6 +536,42 @@ In this application: * `OldArticlesController#show` will use no layout at all * `OldArticlesController#index` will use the `old` layout +##### Template Inheritance + +Similar to the Layout Inheritance logic, if a template or partial is not found in the conventional path, the controller will look for a template or partial to render in its inheritance chain. For example: + +```ruby +# in app/controllers/application_controller +class ApplicationController < ActionController::Base +end + +# in app/controllers/admin_controller +class AdminController < ApplicationController +end + +# in app/controllers/admin/products_controller +class Admin::ProductsController < AdminController + def index + end +end +``` + +The lookup order for a `admin/products#index` action will be: + +* `app/views/admin/products/` +* `app/views/admin/` +* `app/views/application/` + +This makes `app/views/application/` a great place for your shared partials, which can then be rendered in your ERB as such: + +```erb +<%# app/views/admin/products/index.html.erb %> +<%= render @products || "empty_list" %> + +<%# app/views/application/_empty_list.html.erb %> +There are no items in this list <em>yet</em>. +``` + #### Avoiding Double Render Errors Sooner or later, most Rails developers will see the error message "Can only render or redirect once per action". While this is annoying, it's relatively easy to fix. Usually it happens because of a fundamental misunderstanding of the way that `render` works. @@ -1028,9 +1047,14 @@ One way to use partials is to treat them as the equivalent of subroutines: as a <%= render "shared/footer" %> ``` -Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. +Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain +content that is shared by many pages in your application. You don't need to see +the details of these sections when you're concentrating on a particular page. -As you already could see from the previous sections of this guide, `yield` is a very powerful tool for cleaning up your layouts. Keep in mind that it's pure ruby, so you can use it almost everywhere. For example, we can use it to DRY form layout definition for several similar resources: +As seen in the previous sections of this guide, `yield` is a very powerful tool +for cleaning up your layouts. Keep in mind that it's pure Ruby, so you can use +it almost everywhere. For example, we can use it to DRY up form layout +definitions for several similar resources: * `users/index.html.erb` @@ -1039,7 +1063,7 @@ As you already could see from the previous sections of this guide, `yield` is a <p> Name contains: <%= f.text_field :name_contains %> </p> - <%= end %> + <% end %> ``` * `roles/index.html.erb` @@ -1049,7 +1073,7 @@ As you already could see from the previous sections of this guide, `yield` is a <p> Title contains: <%= f.text_field :title_contains %> </p> - <%= end %> + <% end %> ``` * `shared/_search_filters.html.erb` @@ -1114,6 +1138,36 @@ You can also pass local variables into partials, making them even more powerful Although the same partial will be rendered into both views, Action View's submit helper will return "Create Zone" for the new action and "Update Zone" for the edit action. +To pass a local variable to a partial in only specific cases use the `local_assigns`. + +* `index.html.erb` + + ```erb + <%= render user.articles %> + ``` + +* `show.html.erb` + + ```erb + <%= render article, full: true %> + ``` + +* `_articles.html.erb` + + ```erb + <%= content_tag_for :article, article do |article| %> + <h2><%= article.title %></h2> + + <% if local_assigns[:full] %> + <%= simple_format article.body %> + <% else %> + <%= truncate article.body %> + <% end %> + <% end %> + ``` + +This way it is possible to use the partial without the need to declare all local variables. + Every partial also has a local variable with the same name as the partial (minus the underscore). You can pass an object in to this local variable via the `:object` option: ```erb diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 45cdc549f7..50308f505a 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Maintenance Policy for Ruby on Rails ==================================== diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md index 44236ad239..121cf2b185 100644 --- a/guides/source/nested_model_forms.md +++ b/guides/source/nested_model_forms.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Rails Nested Model Forms ======================== @@ -106,7 +106,7 @@ Consider the following typical RESTful controller which will prepare a new Perso class PeopleController < ApplicationController def new @person = Person.new - @person.built_address + @person.build_address 2.times { @person.projects.build } end diff --git a/guides/source/plugins.md b/guides/source/plugins.md index bd884441ac..4e630a39f3 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** The Basics of Creating Rails Plugins ==================================== @@ -265,7 +265,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `rake`. @@ -308,7 +308,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` When you run `rake`, you should see the tests all pass: @@ -382,7 +382,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` Run `rake` one final time and you should see: diff --git a/guides/source/profiling.md b/guides/source/profiling.md index 695b09647f..ce093f78ba 100644 --- a/guides/source/profiling.md +++ b/guides/source/profiling.md @@ -1,4 +1,4 @@ -*DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** A Guide to Profiling Rails Applications ======================================= diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 0db777b9bb..b7364536c3 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Rails Application Templates =========================== @@ -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 561a3d9392..993cd5ac44 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Rails on Rack ============= @@ -254,7 +254,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::RequestId`** -* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#uuid` method. +* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method. **`Rails::Rack::Logger`** diff --git a/guides/source/routing.md b/guides/source/routing.md index 893cedeefc..52f11f92bd 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Rails Routing from the Outside In ================================= @@ -177,6 +177,8 @@ WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) preve ```ruby form_for @geocoder, url: geocoder_path do |f| + +# snippet for brevity ``` ### Controller Namespaces and Routing @@ -229,7 +231,7 @@ or, for a single case: resources :articles, path: '/admin/articles' ``` -In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`: +In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `ArticlesController`: | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ------------------------ | -------------------- | ---------------------- | @@ -807,6 +809,21 @@ 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': + +```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: + +```ruby +mount AdminApp, at: '/admin' +``` + ### Using `root` You can specify what Rails should route `'/'` to with the `root` method: @@ -909,7 +926,7 @@ The `:as` option lets you override the normal naming for the named route helpers resources :photos, as: 'images' ``` -will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers. +will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers. | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ---------------- | ----------------- | -------------------- | @@ -1006,7 +1023,7 @@ TIP: If your application has many RESTful routes, using `:only` and `:except` to ### Translated Paths -Using `scope`, we can alter path names generated by resources: +Using `scope`, we can alter path names generated by `resources`: ```ruby scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 0ecf8a80df..50866350f8 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails Guides Guidelines =============================== @@ -64,7 +64,9 @@ The guides and the API should be coherent and consistent where appropriate. In p HTML Guides ----------- -Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device. +Before generating the guides, make sure that you have the latest version of +Bundler installed on your system. As of this writing, you must install Bundler +1.3.5 or later on your device. To install the latest version of Bundler, run `gem install bundler`. @@ -82,6 +84,8 @@ or bundle exec rake guides:generate:html ``` +Resulting HTML files can be found in the `./output` directory. + To process `my_guide.md` and nothing else use the `ONLY` environment variable: ``` diff --git a/guides/source/security.md b/guides/source/security.md index e4cc79df55..93580d4d4e 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Ruby on Rails Security Guide ============================ @@ -449,14 +449,16 @@ Depending on your web application, there may be more ways to hijack the user's a ### CAPTCHAs -INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ +INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ -But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. +A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails. -The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that they are human, but reveal that a spam robot is a bot. +The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms. -Most bots are really dumb, they crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. +Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. + +Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response. Here are some ideas how to hide honeypot fields by JavaScript and/or CSS: @@ -570,7 +572,7 @@ NOTE: _When sanitizing, protecting or verifying something, prefer whitelists ove A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_: -* Use before_action only: [...] instead of except: [...]. This way you don't forget to turn it off for newly added actions. +* Use before_action except: [...] instead of only: [...] for security-related actions. This way you don't forget to enable security checks for newly added actions. * Allow <strong> instead of removing <script> against Cross-Site Scripting (XSS). See below for details. * Don't try to correct user input by blacklists: * This will make the attack work: "<sc<script>ript>".gsub("<script>", "") @@ -710,7 +712,7 @@ The log files on www.attacker.com will read like this: GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 ``` -You can mitigate these attacks (in the obvious way) by adding the [httpOnly](http://dev.rubyonrails.org/ticket/8895) flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. +You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though. ##### Defacement @@ -923,7 +925,7 @@ HTTP/1.1 200 OK [Second New response created by attacker begins] Content-Type: text/html -<html><font color=red>hey</font></html> [Arbitary malicious input is +<html><font color=red>hey</font></html> [Arbitrary malicious input is Keep-Alive: timeout=15, max=100 shown as the redirected page] Connection: Keep-Alive Transfer-Encoding: chunked @@ -969,7 +971,7 @@ request: | `{ "person": [null, null, ...] }` | `{ :person => [] }` | | `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` | -It is possible to return to old behaviour and disable `deep_munge` configuring +It is possible to return to old behavior and disable `deep_munge` configuring your application if you are aware of the risk and know how to handle it: ```ruby @@ -1029,7 +1031,7 @@ Additional Resources The security landscape shifts and it is important to keep up to date, because missing a new vulnerability can be catastrophic. You can find additional resources about (Rails) security here: -* The Ruby on Rails security project posts security news regularly: [http://www.rorsecurity.info](http://www.rorsecurity.info) * Subscribe to the Rails security [mailing list](http://groups.google.com/group/rubyonrails-security) * [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too) -* A [good security blog](http://ha.ckers.org/blog/) including the [Cross-Site scripting Cheat Sheet](http://ha.ckers.org/xss.html) +* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) + diff --git a/guides/source/testing.md b/guides/source/testing.md index 21b0b37efa..230e8162f3 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** A Guide to Testing Rails Applications ===================================== @@ -59,9 +59,9 @@ You can find comprehensive documentation in the [Fixtures API documentation](htt #### 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 written in YAML. There is one file per model. +_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 fixture stubs will be automatically created and placed in this directory. +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 @@ -82,7 +82,7 @@ steve: 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 space. You can place comments in a fixture file by using the # character in the first column. +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 @@ -120,9 +120,9 @@ user_<%= n %>: Rails by default automatically loads all fixtures from the `test/fixtures` directory for your models and controllers test. Loading involves three steps: -* Remove any existing data from the table corresponding to the fixture -* Load the fixture data into the table -* Dump the fixture data into a method in case you want to access it directly +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)) @@ -138,35 +138,26 @@ users(:david) users(:david).id # one can also access methods available on the User class -email(david.girlfriend.email, david.location_tonight) +email(david.partner.email, david.location_tonight) ``` -### Rake Tasks for Running your Tests - -Rails comes with a number of built-in rake tasks to help with testing. The -table below lists the commands included in the default Rakefile when a Rails -project is created. - -| Tasks | Description | -| ----------------------- | ----------- | -| `rake test` | Runs all tests in the `test` directory. You can also run `rake` and Rails will run all tests by default | -| `rake test:controllers` | Runs all the controller tests from `test/controllers` | -| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional` | -| `rake test:helpers` | Runs all the helper tests from `test/helpers` | -| `rake test:integration` | Runs all the integration tests from `test/integration` | -| `rake test:jobs` | Runs all the job tests from `test/jobs` | -| `rake test:mailers` | Runs all the mailer tests from `test/mailers` | -| `rake test:models` | Runs all the model tests from `test/models` | -| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit` | -| `rake test:db` | Runs all tests in the `test` directory and resets the db | +### Console Tasks for Running your Tests + +Rails comes with a CLI command to run tests. +Here are some examples of how to use it: + +```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. -Unit Testing your Models +Model Testing ------------------------ -In Rails, unit tests are what you write to test your models. - 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: @@ -259,10 +250,10 @@ 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 `rake test` command. +Running a test is as simple as invoking the file containing the test cases through `rails test` command. ```bash -$ bin/rake test test/models/article_test.rb +$ bin/rails test test/models/article_test.rb . Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. @@ -270,10 +261,12 @@ 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/rake test test/models/article_test.rb test_the_truth +$ bin/rails test test/models/article_test.rb test_the_truth . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. @@ -281,8 +274,6 @@ Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` -This will run all test methods from the test case. - 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 @@ -296,10 +287,10 @@ test "should not save article without title" do end ``` -Let us run this newly added test. +Let us run this newly added test (where `6` is the number of line where the test is defined). ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. @@ -339,7 +330,7 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. @@ -347,7 +338,11 @@ Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` -Now, if you noticed, we first wrote a test which fails for a desired functionality, then we wrote some code which adds the functionality and finally we ensured that our test passes. This approach to software development is referred to as _Test-Driven Development_ (TDD). +Now, if you noticed, we first wrote a test which fails for a desired +functionality, then we wrote some code which adds the functionality and finally +we ensured that our test passes. This approach to software development is +referred to as +[_Test-Driven Development_ (TDD)](http://c2.com/cgi/wiki?TestDrivenDevelopment). #### What an error looks like @@ -364,7 +359,7 @@ end Now you can see even more output in the console from running the tests: ```bash -$ bin/rake test test/models/article_test.rb test_should_report_error +$ bin/rails test test/models/article_test.rb E Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. @@ -379,17 +374,20 @@ NameError: undefined local variable or method `some_undefined_variable' for #<Ar Notice the 'E' in the output. It denotes a test with error. -NOTE: The execution of each test method stops as soon as any error or an assertion failure is encountered, and the test suite continues with the next method. All test methods are executed in alphabetical order. +NOTE: The execution of each test method stops as soon as any error or an +assertion failure is encountered, and the test suite continues with the next +method. All test methods are executed in random order. The +[`config.active_support.test_order` option](configuring.html#configuring-active-support) +can be used to configure test order. When a test fails you are presented with the corresponding backtrace. By default Rails filters that backtrace and will only print lines relevant to your application. This eliminates the framework noise and helps to focus on your code. However there are situations when you want to see the full -backtrace. simply set the `BACKTRACE` environment variable to enable this -behavior: +backtrace. Simply set the `-b` (or `--backtrace`) argument to enable this behavior: ```bash -$ BACKTRACE=1 bin/rake test test/models/article_test.rb +$ bin/rails test -b test/models/article_test.rb ``` If we want this test to pass we can modify it to use `assert_raises` like so: @@ -409,9 +407,49 @@ This test should now pass. By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned. -There are a bunch of different types of assertions you can use that come with [`Minitest`](https://github.com/seattlerb/minitest), the default testing library used by Rails. - -For a list of all available assertions please check the [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically [`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) +Here's an extract of the assertions you can use with +[`Minitest`](https://github.com/seattlerb/minitest), the default testing library +used by Rails. The `[msg]` parameter is an optional string message you can +specify to make your test failure messages clearer. It's not required. + +| Assertion | Purpose | +| ---------------------------------------------------------------- | ------- | +| `assert( test, [msg] )` | Ensures that `test` is true.| +| `assert_not( test, [msg] )` | Ensures that `test` is false.| +| `assert_equal( expected, actual, [msg] )` | Ensures that `expected == actual` is true.| +| `assert_not_equal( expected, actual, [msg] )` | Ensures that `expected != actual` is true.| +| `assert_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is true.| +| `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.| +| `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.| +| `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.| +| `assert_empty( obj, [msg] )` | Ensures that `obj` is `empty?`.| +| `assert_not_empty( obj, [msg] )` | Ensures that `obj` is not `empty?`.| +| `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.| +| `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_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.| +| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| +| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.| +| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.| +| `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.| +| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.| +| `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| +| `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.| +| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| +| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`| +| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`| +| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?| +| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| + +The above are a subset of assertions that minitest supports. For an exhaustive & +more up-to-date list, please check +[Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically +[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier. @@ -424,12 +462,11 @@ Rails adds some custom assertions of its own to the `minitest` framework: | Assertion | Purpose | | --------------------------------------------------------------------------------- | ------- | | `assert_difference(expressions, difference = 1, message = nil) {...}` | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| -| `assert_no_difference(expressions, message = nil, &block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| +| `assert_no_difference(expressions, message = nil, &block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| | `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| | `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | 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. @@ -442,10 +479,11 @@ All the basic assertions such as `assert_equal` defined in `Minitest::Assertions * `ActionMailer::TestCase` * `ActionView::TestCase` * `ActionDispatch::IntegrationTest` +* `ActiveJob::TestCase` Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. -NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html) +NOTE: For more information on `Minitest`, refer to [Minitest](http://docs.seattlerb.org/minitest) Functional Tests for Your Controllers ------------------------------------- @@ -467,34 +505,43 @@ Now that we have used Rails scaffold generator for our `Article` resource, it ha 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: -* The action of the controller you are requesting. This can be in the form of a string or a symbol. -* An optional hash of request parameters to pass into the action (eg. query string parameters or article variables). -* An optional hash of session variables to pass along with the request. -* An optional hash of flash values. +* The action of the controller you are requesting. + This can be in the form of a string or a symbol. + +* `params`: option with a hash of request parameters to pass into the action + (e.g. query string parameters or article variables). + +* `session`: option with a hash of session variables to pass along with the request. + +* `flash`: option with a hash of flash values. + +All the keyword arguments are optional. Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting a `user_id` of 5 in the session: ```ruby -get(:show, {'id' => "12"}, {'user_id' => 5}) +get(:show, params: { id: 12 }, session: { user_id: 5 }) ``` Another example: Calling the `:view` action, passing an `id` of 12 as the `params`, this time with no session, but with a flash message. ```ruby -get(:view, {'id' => '12'}, nil, {'message' => 'booya!'}) +get(:view, params: { id: 12 }, flash: { message: 'booya!' }) ``` NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. @@ -504,10 +551,10 @@ Let us modify `test_should_create_article` test in `articles_controller_test.rb` ```ruby test "should create article" do assert_difference('Article.count') do - post :create, article: {title: 'Some title'} + post :create, params: { article: { title: 'Some title' } } end - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(Article.last) end ``` @@ -528,24 +575,34 @@ All of request types have equivalent methods that you can use. In a typical C.R. NOTE: Functional tests do not verify whether the specified request type is accepted by the action, we're more concerned with the result. Request tests exist for this use case to make your tests more purposeful. +### Testing XHR (AJAX) requests + +To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`, +`patch`, `put`, and `delete` methods: + +```ruby +test "ajax request" do + get :show, params: { id: articles(:first).id }, xhr: true + + assert_equal 'hello world', @response.body + assert_equal "text/javascript", @response.content_type +end +``` + ### The Four Hashes of the Apocalypse After a request has been made and processed, you will have 4 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 @@ -573,46 +630,6 @@ 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 creating Articles 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`. @@ -625,10 +642,10 @@ Let's start by adding this assertion to our `test_should_create_article` test: ```ruby test "should create article" do assert_difference('Article.count') do - post :create, article: {title: 'Some title'} + 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 ``` @@ -636,7 +653,7 @@ end If we run our test now, we should see a failure: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article Run options: -n test_should_create_article --seed 32266 # Running: @@ -674,7 +691,7 @@ end Now if we run our tests, we should see it pass: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article Run options: -n test_should_create_article --seed 18981 # Running: @@ -695,7 +712,7 @@ Let's write a test for the `:show` action: ```ruby test "should show article" do article = articles(:one) - get :show, id: article.id + get :show, params: { id: article.id } assert_response :success end ``` @@ -708,7 +725,7 @@ How about deleting an existing Article? test "should destroy article" do article = articles(:one) assert_difference('Article.count', -1) do - delete :destroy, id: article.id + delete :destroy, params: { id: article.id } end assert_redirected_to articles_path @@ -720,8 +737,8 @@ We can also add a test for updating an existing Article. ```ruby test "should update article" do article = articles(:one) - patch :update, id: article.id, article: {title: "updated"} - assert_redirected_to article_path(assigns(:article)) + patch :update, params: { id: article.id, article: { title: "updated" } } + assert_redirected_to article_path(article) end ``` @@ -740,35 +757,67 @@ class ArticlesControllerTest < ActionController::TestCase # called after every single test def teardown - # as we are re-initializing @article before every test - # setting it to nil here is not essential but I hope - # you understand how you can use the teardown method - @article = nil + # when controller is using cache it may be a good idea to reset it afterwards + Rails.cache.clear end test "should show article" do # Reuse the @article instance variable from setup - get :show, id: @article.id + get :show, params: { id: @article.id } assert_response :success end test "should destroy article" do assert_difference('Article.count', -1) do - delete :destroy, id: @article.id + delete :destroy, params: { id: @article.id } end assert_redirected_to articles_path end test "should update article" do - patch :update, id: @article.id, article: {title: "updated"} - assert_redirected_to article_path(assigns(:article)) + patch :update, params: { id: @article.id, article: { title: "updated" } } + assert_redirected_to article_path(@article) end end ``` Similar to other callbacks in Rails, the `setup` and `teardown` methods can also be used by passing a block, lambda, or method name as a symbol to call. +### Test helpers + +To avoid code duplication, you can add your own test helpers. +Sign in helper can be a good example: + +```ruby +test/test_helper.rb + +module SignInHelper + def sign_in(user) + session[:user_id] = user.id + end +end + +class ActionController::TestCase + include SignInHelper +end +``` + +```ruby +require 'test_helper' + +class ProfileControllerTest < ActionController::TestCase + + test "should show profile" do + # helper is now reusable from any controller test case + sign_in users(:david) + + get :show + assert_response :success + end +end +``` + Testing Routes -------------- @@ -789,7 +838,7 @@ end I've added this file here `test/controllers/articles_routes_test.rb` and if we run the test we should see: ```bash -$ bin/rake test test/controllers/articles_routes_test.rb +$ bin/rails test test/controllers/articles_routes_test.rb # Running: @@ -805,7 +854,7 @@ For more information on routing assertions available in Rails, see the API docum Testing Views ------------- -Testing the response to your request by asserting the presence of key HTML elements and their content is a common way to test the views of your application. The `assert_select` method allows you to query HTML elements of the response by using a simple yet powerful syntax. +Testing the response to your request by asserting the presence of key HTML elements and their content is a common way to test the views of your application. Like route tests, view tests reside in `test/controllers/` or are part of controller tests. The `assert_select` method allows you to query HTML elements of the response by using a simple yet powerful syntax. There are two forms of `assert_select`: @@ -866,7 +915,7 @@ assert_select_email do end ``` -Testing helpers +Testing Helpers --------------- In order to test helpers, all you need to do is check that the output of the @@ -978,13 +1027,12 @@ How about testing our ability to create a new article in our blog and see the re test "can create an article" do get "/articles/new" assert_response :success - assert_template "articles/new", partial: "articles/_form" - post "/articles", article: {title: "can create", body: "article successfully."} + 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 ``` @@ -996,7 +1044,8 @@ We start by calling the `:new` action on our Articles controller. This response After this we make a post request to the `:create` action of our Articles controller: ```ruby -post "/articles", article: {title: "can create", body: "article successfully."} +post "/articles", + params: { article: { title: "can create", body: "article successfully." } } assert_response :redirect follow_redirect! ``` @@ -1018,7 +1067,7 @@ Testing mailer classes requires some specific tools to do a thorough job. ### Keeping the Postman in Check -Your mailer classes - like every other part of your Rails application - should be tested to ensure that it is working as expected. +Your mailer classes - like every other part of your Rails application - should be tested to ensure that they are working as expected. The goals of testing your mailer classes are to ensure that: @@ -1050,9 +1099,10 @@ require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "invite" do # Send the email, then test that it got queued - email = UserMailer.create_invite('me@example.com', - 'friend@example.com', Time.now).deliver_now - assert_not ActionMailer::Base.deliveries.empty? + assert_emails 1 do + email = UserMailer.create_invite('me@example.com', + 'friend@example.com', Time.now).deliver_now + end # Test the body of the sent email contains what we expect it to assert_equal ['me@example.com'], email.from @@ -1100,7 +1150,7 @@ require 'test_helper' class UserControllerTest < ActionController::TestCase test "invite friend" do assert_difference 'ActionMailer::Base.deliveries.size', +1 do - post :invite_friend, email: 'friend@example.com' + post :invite_friend, params: { email: 'friend@example.com' } end invite_email = ActionMailer::Base.deliveries.last @@ -1154,7 +1204,7 @@ within a model: ```ruby require 'test_helper' -class ProductTest < ActiveSupport::TestCase +class ProductTest < ActiveJob::TestCase test 'billing job scheduling' do assert_enqueued_with(job: BillingJob) do product.charge(account) diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index c0c94475e0..17309d4b47 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** A Guide for Upgrading Ruby on Rails =================================== @@ -20,7 +20,7 @@ The best way to be sure that your application still works after upgrading is to Rails generally stays close to the latest released Ruby version when it's released: -* Rails 5 requires Ruby 2.2 or newer. +* Rails 5 requires Ruby 2.2.2 or newer. * Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer. * Rails 3.2.x is the last branch to support Ruby 1.8.7. * Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible. @@ -75,6 +75,23 @@ 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 + +In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this +behavior has changed to now inherit from `ApplicationJob`. + +When upgrading from Rails 4.2 to Rails 5.0 you need to create an +`application_job.rb` file in `app/jobs/` and add the following content: + +``` +class ApplicationJob < ActiveJob::Base +end +``` + +Then make sure that all your job classes inherit from it. + +See [#19034](https://github.com/rails/rails/pull/19034) for more details. + Upgrading from Rails 4.1 to Rails 4.2 ------------------------------------- @@ -276,6 +293,22 @@ class Notifier < ActionMailer::Base end ``` +### Foreign Key Support + +The migration DSL has been expanded to support foreign key definitions. If +you've been using the Foreigner gem, you might want to consider removing it. +Note that the foreign key support of Rails is a subset of Foreigner. This means +that not every Foreigner definition can be fully replaced by its Rails +migration DSL counterpart. + +The migration procedure is as follows: + +1. remove `gem "foreigner"` from the Gemfile. +2. run `bundle install`. +3. run `bin/rake db:schema:dump`. +4. make sure that `db/schema.rb` contains every foreign key definition with +the necessary options. + Upgrading from Rails 4.0 to Rails 4.1 ------------------------------------- @@ -427,7 +460,7 @@ If your application currently depend on MultiJSON directly, you have a few optio WARNING: Do not simply replace `MultiJson.dump` and `MultiJson.load` with `JSON.dump` and `JSON.load`. These JSON gem APIs are meant for serializing and -deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html#method-i-load). +deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/json/rdoc/JSON.html#method-i-load). #### JSON gem compatibility @@ -534,7 +567,7 @@ module FixtureFileHelpers Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) end end -ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers +ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers ``` ### I18n enforcing available locales @@ -850,6 +883,20 @@ this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. * To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders). +* Rails 4.0 has changed to default join table for `has_and_belongs_to_many` relations to strip the common prefix off the second table name. Any existing `has_and_belongs_to_many` relationship between models with a common prefix must be specified with the `join_table` option. For example: + +```ruby +CatalogCategory < ActiveRecord::Base + has_and_belongs_to_many :catalog_products, join_table: 'catalog_categories_catalog_products' +end + +CatalogProduct < ActiveRecord::Base + has_and_belongs_to_many :catalog_categories, join_table: 'catalog_categories_catalog_products' +end +``` + +* Note that the the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly. + ### Active Resource Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. @@ -879,7 +926,7 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur Please note that you should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new `secret_key_base` in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing `secret_token` in place, not set the new `secret_key_base`, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete. -If you are relying on the ability for external applications or Javascript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. +If you are relying on the ability for external applications or JavaScript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. * Rails 4.0 encrypts the contents of cookie-based sessions if `secret_key_base` has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty. @@ -893,6 +940,8 @@ Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for d * Rails 4.0 has removed the XML parameters parser. You will need to add the `actionpack-xml_parser` gem if you require this feature. +* Rails 4.0 changes the default `layout` lookup set using symbols or procs that return nil. To get the "no layout" behavior, return false instead of nil. + * Rails 4.0 changes the default memcached client from `memcache-client` to `dalli`. To upgrade, simply add `gem 'dalli'` to your `Gemfile`. * Rails 4.0 deprecates the `dom_id` and `dom_class` methods in controllers (they are fine in views). You will need to include the `ActionView::RecordIdentifier` module in controllers requiring this feature. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 5131f809d7..f3d3a83afc 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -1,4 +1,4 @@ -**DO NOT READ THIS FILE IN GITHUB, GUIDES ARE PUBLISHED IN http://guides.rubyonrails.org.** +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** Working with JavaScript in Rails ================================ @@ -357,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/DOM/Manipulating_the_browser_history#The_pushState(\).C2.A0method), +[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState()_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 index 96e4153165..933e5ee9a2 100644 --- a/install.rb +++ b/install.rb @@ -7,10 +7,10 @@ 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` + `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-document && rm #{framework}-#{version}.gem` end puts "Installing rails..." `gem build rails.gemspec` -`gem install rails-#{version}.gem --no-ri --no-rdoc ` +`gem install rails-#{version}.gem --no-document ` `rm rails-#{version}.gem` diff --git a/rails.gemspec b/rails.gemspec index b3143e6fe1..52112680b2 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Full-stack web application framework.' s.description = 'Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.required_rubygems_version = '>= 1.8.11' s.license = 'MIT' @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = ['README.md'] + Dir['guides/**/*'] - Dir['guides/output/**/*'] + s.files = ['README.md'] s.add_dependency 'activesupport', version s.add_dependency 'actionpack', version diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 623f44d56a..3536929d00 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,145 @@ +* 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 + `index`. E.g. to serve `main.html` instead of `index.html` for directory + requests, set `config.static_index` to `"main"`. + + *Eliot Sykes* + +* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`). + + *Mohnish Thallavajhula* + +* Fix mailer previews with attachments by using the mail gem's own API to + locate the first part of the correct mime type. + + Fixes #14435. + + *Andrew White* + +* Remove sqlite support from `rails dbconsole`. + + *Andrew White* + +* Rename `railties/bin` to `railties/exe` to match the new Bundler executables + convention. + + *Islam Wazery* + +* Print `bundle install` output in `rails new` as soon as it's available. + + Running `rails new` will now print the output of `bundle install` as + it is available, instead of waiting until all gems finish installing. + + *Max Holder* + +* Respect `pluralize_table_names` when generating fixture file. + + Fixes #19519. + + *Yuji Yaginuma* + +* Add a new-line to the end of route method generated code. + + We need to add a `\n`, because we cannot have two routes + in the same line. + + *arthurnn* + +* Add `rake initializers`. + + This task prints out all defined initializers in the order they are invoked + by Rails. This is helpful for debugging issues related to the initialization + process. + + *Naoto Kaneko* + +* Created rake restart task. Restarts your Rails app by touching the + `tmp/restart.txt`. + + Fixes #18876. + + *Hyonjee Joo* + +* Add `config/initializers/active_record_belongs_to_required_by_default.rb`. + + Newly generated Rails apps have a new initializer called + `active_record_belongs_to_required_by_default.rb` which sets the value of + the configuration option `config.active_record.belongs_to_required_by_default` + to `true` when ActiveRecord is not skipped. + + As a result, new Rails apps require `belongs_to` association on model + to be valid. + + This initializer is *not* added when running `rake rails:update`, so + old apps ported to Rails 5 will work without any change. + + *Josef Šimánek* + +* `delete` operations in configurations are run last in order to eliminate + 'No such middleware' errors when `insert_before` or `insert_after` are added + after the `delete` operation for the middleware being deleted. + + Fixes #16433. + + *Guo Xiang Tan* + +* Newly generated applications get a `README.md` in Markdown. + + *Xavier Noria* + +* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. + + *Xavier Noria* + +* Force generated routes to be inserted into `config/routes.rb`. + + *Andrew White* + +* Don't remove all line endings from `config/routes.rb` when revoking scaffold. + + Fixes #15913. + + *Andrew White* + +* Rename `--skip-test-unit` option to `--skip-test` in app generator + + *Melanie Gilman* + +* Add the `method_source` gem to the default Gemfile for apps. + + *Sean Griffin* + +* Drop old test locations from `rake stats`: + + - test/functional + - test/unit + + *Ravil Bayramgalin* + +* Update `rake stats` to correctly count declarative tests + as methods in `_test.rb` files. + + *Ravil Bayramgalin* + * Remove deprecated `test:all` and `test:all:db` tasks. *Rafael Mendonça França* @@ -18,7 +160,7 @@ *Andrew Kozlov* -* Add `config/initializers/callback_terminator.rb` +* Add `config/initializers/callback_terminator.rb`. Newly generated Rails apps have a new initializer called `callback_terminator.rb` which sets the value of the configuration option diff --git a/railties/Rakefile b/railties/Rakefile index 9a377ce4ee..4393f45790 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' task :default => :test @@ -31,20 +30,3 @@ Rake::TestTask.new('test:regular') do |t| t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - -# Generate GEM ---------------------------------------------------------------------------- - -spec = eval(File.read('railties.gemspec')) - -Gem::PackageTask.new(spec) do |pkg| - pkg.gem_spec = spec -end - -# Publishing ------------------------------------------------------- - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/railties/bin/rails b/railties/exe/rails index 82c17cabce..82c17cabce 100755 --- a/railties/bin/rails +++ b/railties/exe/rails diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index b1f7c29b5a..fe789f3c2a 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -52,6 +52,11 @@ module Rails end end + # Returns a Pathname object of the current rails project, + # otherwise it returns nil if there is no project: + # + # Rails.root + # # => #<Pathname:/Users/someuser/some/path/project> def root application && application.config.root end @@ -94,6 +99,11 @@ module Rails groups end + # Returns a Pathname object of the public folder of the current + # rails project, otherwise it returns nil if there is no project: + # + # Rails.public_path + # # => #<Pathname:/Users/someuser/some/path/project/public> def public_path application && Pathname.new(application.paths["public"].first) end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index 4d49244807..a082932632 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -152,19 +152,5 @@ module Rails File.read('RAILS_VERSION').strip end end - - class AppTask < Task - def component_root_dir(gem_name) - $:.grep(%r{#{gem_name}[\w.-]*/lib\z}).first[0..-5] - end - - def api_dir - 'doc/api' - end - - def rails_version - Rails::VERSION::STRING - end - end end end diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_loader.rb index 39d8007333..a9fe21824e 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_loader.rb @@ -1,7 +1,8 @@ require 'pathname' +require 'rails/version' module Rails - module AppRailsLoader + module AppLoader # :nodoc: extend self RUBY = Gem.ruby @@ -9,7 +10,7 @@ module Rails BUNDLER_WARNING = <<EOS Looks like your app's ./bin/rails is a stub that was generated by Bundler. -In Rails 4, your app's bin/ directory contains executables that are versioned +In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned like any other source code, rather than stubs that are generated on demand. Here's how to upgrade: @@ -28,7 +29,7 @@ generate it and add it to source control: EOS - def exec_app_rails + def exec_app original_cwd = Dir.pwd loop do diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8da73db821..8075068b3f 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'yaml' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' @@ -6,8 +7,7 @@ require 'active_support/message_verifier' require 'rails/engine' module Rails - # In Rails 3.0, a Rails::Application object was introduced which is nothing more than - # an Engine but with the responsibility of coordinating the whole boot process. + # An Engine with the responsibility of coordinating the whole boot process. # # == Initialization # @@ -159,8 +159,9 @@ module Rails # Implements call according to the Rack API. It simply # dispatches the request to the underlying middleware stack. def call(env) - env["ORIGINAL_FULLPATH"] = build_original_fullpath(env) - env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"] + req = ActionDispatch::Request.new env + env["ORIGINAL_FULLPATH"] = req.fullpath + env["ORIGINAL_SCRIPT_NAME"] = req.script_name super(env) end @@ -227,7 +228,6 @@ module Rails yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") if yaml.exist? - require "yaml" require "erb" (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {} else @@ -427,9 +427,9 @@ module Rails # Return an array of railties respecting the order they're loaded # and the order specified by the +railties_order+ config. # - # While when running initializers we need engines in reverse - # order here when copying migrations from railties we need then in the same - # order as given by +railties_order+ + # While running initializers we need engines in reverse order here when + # copying migrations from railties ; we need them in the order given by + # +railties_order+. def migration_railties # :nodoc: ordered_railties.flatten - [self] end @@ -504,25 +504,13 @@ module Rails default_stack.build_stack end - def build_original_fullpath(env) #:nodoc: - path_info = env["PATH_INFO"] - query_string = env["QUERY_STRING"] - script_name = env["SCRIPT_NAME"] - - if query_string.present? - "#{script_name}#{path_info}?#{query_string}" - else - "#{script_name}#{path_info}" - end - end - def validate_secret_key_config! #:nodoc: if secrets.secret_key_base.blank? ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " + "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? - raise "Missing `secret_token` and `secret_key_base` for '#{Rails.env}' environment, set these values in `config/secrets.yml`" + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 2821c8d757..4fc7a1db62 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -6,17 +6,17 @@ require 'rails/source_annotation_extractor' module Rails class Application class Configuration < ::Rails::Engine::Configuration - attr_accessor :allow_concurrency, :asset_host, :assets, :autoflush_log, + attr_accessor :allow_concurrency, :asset_host, :autoflush_log, :cache_classes, :cache_store, :consider_all_requests_local, :console, :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :railties_order, :relative_url_root, :secret_key_base, :secret_token, - :serve_static_files, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change, + :serve_static_files, :ssl_options, :static_cache_control, :static_index, + :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x attr_writer :log_level - attr_reader :encoding + attr_reader :encoding, :api_only def initialize(*) super @@ -28,6 +28,7 @@ module Rails @helpers_paths = [] @serve_static_files = true @static_cache_control = nil + @static_index = "index" @force_ssl = false @ssl_options = {} @session_store = :cookie_store @@ -48,22 +49,8 @@ module Rails @eager_load = nil @secret_token = nil @secret_key_base = nil + @api_only = false @x = Custom.new - - @assets = ActiveSupport::OrderedOptions.new - @assets.enabled = true - @assets.paths = [] - @assets.precompile = [ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) }, - /(?:\/|\\|\A)application\.(css|js)$/ ] - @assets.prefix = "/assets" - @assets.version = '1.0' - @assets.debug = false - @assets.compile = true - @assets.digest = false - @assets.cache_store = [ :file_store, "#{root}/tmp/cache/assets/#{Rails.env}/" ] - @assets.js_compressor = nil - @assets.css_compressor = nil - @assets.logger = nil end def encoding=(value) @@ -74,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 02eea82b0c..6f9ccec137 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 + middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control, index: config.static_index end if rack_cache = load_rack_cache @@ -28,7 +28,7 @@ module Rails middleware.use ::Rack::Lock unless allow_concurrency? 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 +42,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 diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 7a1bb1e25c..0599e988d9 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -108,6 +108,13 @@ module Rails ActionDispatch::Reloader.to_cleanup(&callback) end end + + # Disable dependency loading during request cycle + initializer :disable_dependency_loading do + if config.eager_load && config.cache_classes + ActiveSupport::Dependencies.unhook! + end + end end end end diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index dd70c272c6..a8794bc0de 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -1,8 +1,8 @@ -require 'rails/app_rails_loader' +require 'rails/app_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. -Rails::AppRailsLoader.exec_app_rails +Rails::AppLoader.exec_app require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 27779857b7..fd352dc9b7 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -7,9 +7,7 @@ class CodeStatistics #:nodoc: 'Model tests', 'Mailer tests', 'Job tests', - 'Integration tests', - 'Functional tests (old)', - 'Unit tests (old)'] + 'Integration tests'] def initialize(*pairs) @pairs = pairs @@ -43,11 +41,9 @@ class CodeStatistics #:nodoc: if File.directory?(path) && (/^\./ !~ file_name) stats.add(calculate_directory_statistics(path, pattern)) + elsif file_name =~ pattern + stats.add_by_file_path(path) end - - next unless file_name =~ pattern - - stats.add_by_file_path(path) end stats @@ -73,12 +69,12 @@ class CodeStatistics #:nodoc: def print_header print_splitter - puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" print_splitter end def print_splitter - puts "+----------------------+-------+-------+---------+---------+-----+-------+" + puts "+----------------------+--------+--------+---------+---------+-----+-------+" end def print_line(name, statistics) @@ -86,8 +82,8 @@ class CodeStatistics #:nodoc: loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 puts "| #{name.ljust(20)} " \ - "| #{statistics.lines.to_s.rjust(5)} " \ - "| #{statistics.code_lines.to_s.rjust(5)} " \ + "| #{statistics.lines.to_s.rjust(6)} " \ + "| #{statistics.code_lines.to_s.rjust(6)} " \ "| #{statistics.classes.to_s.rjust(7)} " \ "| #{statistics.methods.to_s.rjust(7)} " \ "| #{m_over_c.to_s.rjust(3)} " \ diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb index 60e4aef9b7..a142236dbe 100644 --- a/railties/lib/rails/code_statistics_calculator.rb +++ b/railties/lib/rails/code_statistics_calculator.rb @@ -24,6 +24,8 @@ class CodeStatisticsCalculator #:nodoc: } } + PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/ + def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0) @lines = lines @code_lines = code_lines @@ -74,6 +76,10 @@ class CodeStatisticsCalculator #:nodoc: private def file_type(file_path) - File.extname(file_path).sub(/\A\./, '').downcase.to_sym + if file_path.end_with? '_test.rb' + :minitest + else + File.extname(file_path).sub(/\A\./, '').downcase.to_sym + end end end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index f32bf772a5..12bd73db24 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -6,7 +6,8 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test", } command = ARGV.shift diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index 8bae08e44e..685d55eea8 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -14,6 +14,7 @@ The most common rails commands are: generate Generate new code (short-cut alias: "g") console Start the Rails console (short-cut alias: "c") server Start the Rails server (short-cut alias: "s") + test Run tests (short-cut alias: "t") dbconsole Start a console for the database specified in config/database.yml (short-cut alias: "db") new Create a new Rails application. "rails new my_app" creates a @@ -27,7 +28,7 @@ In addition to those, there are: All commands can be run with -h (or --help) for more information. EOT - COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help) + COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) def initialize(argv) @argv = argv @@ -81,6 +82,10 @@ EOT end end + def test + require_command!("test") + end + def dbconsole require_command!("dbconsole") Rails::DBConsole.start @@ -146,6 +151,17 @@ EOT puts HELP_MESSAGE end + # Output an error message stating that the attempted command is not a valid rails command. + # Run the attempted command as a rake command with the --dry-run flag. If successful, suggest + # to the user that they possibly meant to run the given rails command as a rake command. + # Append the help message. + # + # Example: + # $ rails db:migrate + # Error: Command 'db:migrate' not recognized + # Did you mean: `$ rake db:migrate` ? + # (Help message output) + # def write_error_message(command) puts "Error: Command '#{command}' not recognized" if %x{rake #{command} --dry-run 2>&1 } && $?.success? diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 35a815a34f..ea5d20ea24 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -1,14 +1,13 @@ require 'optparse' require 'irb' require 'irb/completion' +require 'rails/commands/console_helper' module Rails class Console - class << self - def start(*args) - new(*args).start - end + include ConsoleHelper + class << self def parse_arguments(arguments) options = {} @@ -21,23 +20,8 @@ module Rails opt.parse!(arguments) end - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env - else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env - end - end - - options + set_options_env(arguments, options) end - - private - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end end attr_reader :options, :app, :console @@ -57,12 +41,9 @@ module Rails end def environment - options[:environment] ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' - end - - def environment? - environment + options[:environment] ||= super end + alias_method :environment?, :environment def set_environment! Rails.env = environment @@ -79,7 +60,7 @@ module Rails end if defined?(console::ExtendCommandBundle) - console::ExtendCommandBundle.send :include, Rails::ConsoleMethods + console::ExtendCommandBundle.include(Rails::ConsoleMethods) end console.start end diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb new file mode 100644 index 0000000000..8ee0b60012 --- /dev/null +++ b/railties/lib/rails/commands/console_helper.rb @@ -0,0 +1,34 @@ +require 'active_support/concern' + +module Rails + module ConsoleHelper # :nodoc: + extend ActiveSupport::Concern + + module ClassMethods + def start(*args) + new(*args).start + end + + private + def set_options_env(arguments, options) + if arguments.first && arguments.first[0] != '-' + env = arguments.first + if available_environments.include? env + options[:environment] = env + else + options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env + end + end + options + end + + def available_environments + Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } + end + end + + def environment + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 0d8b3de0eb..dca60f948f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -1,14 +1,49 @@ require 'erb' require 'yaml' require 'optparse' -require 'rbconfig' +require 'rails/commands/console_helper' module Rails class DBConsole + include ConsoleHelper + attr_reader :arguments - def self.start - new.start + class << self + def parse_arguments(arguments) + options = {} + + OptionParser.new do |opt| + opt.banner = "Usage: rails dbconsole [environment] [options]" + opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| + options['include_password'] = true + end + + opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], + "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| + options['mode'] = mode + end + + opt.on("--header") do |h| + options['header'] = h + end + + opt.on("-h", "--help", "Show this help message.") do + puts opt + exit + end + + opt.on("-e", "--environment=name", String, + "Specifies the environment to run this console under (test/development/production).", + "Default: development" + ) { |v| options[:environment] = v.strip } + + opt.parse!(arguments) + abort opt.to_s unless (0..1).include?(arguments.size) + end + + set_options_env(arguments, options) + end end def initialize(arguments = ARGV) @@ -16,7 +51,7 @@ module Rails end def start - options = parse_arguments(arguments) + options = self.class.parse_arguments(arguments) ENV['RAILS_ENV'] = options[:environment] || environment case config["adapter"] @@ -51,9 +86,6 @@ module Rails ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password'] find_cmd_and_exec('psql', config["database"]) - when "sqlite" - find_cmd_and_exec('sqlite', config["database"]) - when "sqlite3" args = [] @@ -90,7 +122,7 @@ module Rails find_cmd_and_exec("sqsh", *args) else - abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" + abort "Unknown command-line client for #{config['database']}." end end @@ -105,88 +137,37 @@ module Rails end def environment - if Rails.respond_to?(:env) - Rails.env - else - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end + Rails.respond_to?(:env) ? Rails.env : super end protected + def configurations + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end - def configurations - require APP_PATH - ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails dbconsole [environment] [options]" - opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| - options['include_password'] = true - end - - opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], - "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| - options['mode'] = mode - end + def find_cmd_and_exec(commands, *args) + commands = Array(commands) - opt.on("--header") do |h| - options['header'] = h + dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG['EXEEXT']).empty? + commands = commands.map{|cmd| "#{cmd}#{ext}"} end - opt.on("-h", "--help", "Show this help message.") do - puts opt - exit + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end end - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development" - ) { |v| options[:environment] = v.strip } - - opt.parse!(arguments) - abort opt.to_s unless (0..1).include?(arguments.size) - end - - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env + if found + exec full_path_command, *args else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") end end - - options - end - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end - - def find_cmd_and_exec(commands, *args) - commands = Array(commands) - - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - commands += commands.map{|cmd| "#{cmd}.exe"} if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ - - full_path_command = nil - found = commands.detect do |cmd| - dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - File.file?(full_path_command) && File.executable?(full_path_command) - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end end end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 3a71f8d3f8..86bce9b2fe 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -1,5 +1,4 @@ require 'optparse' -require 'rbconfig' options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup } code_or_file = nil diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index bc8f1a8dea..546d3725d8 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -91,10 +91,6 @@ module Rails Hash.new(middlewares) end - def log_path - "log/#{options[:environment]}.log" - end - def default_options super.merge({ Port: 3000, diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb new file mode 100644 index 0000000000..fe5307788a --- /dev/null +++ b/railties/lib/rails/commands/test.rb @@ -0,0 +1,5 @@ +require "rails/test_unit/minitest_plugin" + +$: << File.expand_path("../../test", APP_PATH) + +exit Minitest.run(ARGV) diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index f99cec04c5..d99d27a756 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -35,6 +35,7 @@ module Rails class MiddlewareStackProxy def initialize @operations = [] + @delete_operations = [] end def insert_before(*args, &block) @@ -56,7 +57,7 @@ module Rails end def delete(*args, &block) - @operations << [__method__, args, block] + @delete_operations << [__method__, args, block] end def unshift(*args, &block) @@ -64,15 +65,16 @@ module Rails end def merge_into(other) #:nodoc: - @operations.each do |operation, args, block| + (@operations + @delete_operations).each do |operation, args, block| other.send(operation, *args, &block) end + other end 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 @@ -81,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 a338f31f15..1dede32dd4 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -6,7 +6,7 @@ require 'pathname' module Rails # <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of # functionality and share it with other applications or within a larger packaged application. - # Since Rails 3.0, every <tt>Rails::Application</tt> is just an engine, which allows for simple + # Every <tt>Rails::Application</tt> is just an engine, which allows for simple # feature and application sharing. # # Any <tt>Rails::Engine</tt> is also a <tt>Rails::Railtie</tt>, so the same @@ -15,10 +15,9 @@ module Rails # # == Creating an Engine # - # In Rails versions prior to 3.0, your gems automatically behaved as engines, however, - # this coupled Rails to Rubygems. Since Rails 3.0, if you want a gem to automatically - # behave as an engine, you have to specify an +Engine+ for it somewhere inside - # your plugin's +lib+ folder (similar to how we specify a +Railtie+): + # If you want a gem to behave as an engine, you have to specify an +Engine+ + # for it somewhere inside your plugin's +lib+ folder (similar to how we + # specify a +Railtie+): # # # lib/my_engine.rb # module MyEngine @@ -69,10 +68,9 @@ module Rails # # == Paths # - # Since Rails 3.0, applications and engines have more flexible path configuration (as - # opposed to the previous hardcoded path configuration). This means that you are not - # required to place your controllers at <tt>app/controllers</tt>, but in any place - # which you find convenient. + # Applications and engines have flexible path configuration, meaning that you + # are not required to place your controllers at <tt>app/controllers</tt>, but + # in any place which you find convenient. # # For example, let's suppose you want to place your controllers in <tt>lib/controllers</tt>. # You can set that as an option: @@ -206,42 +204,51 @@ module Rails # With such an engine, everything that is inside the +MyEngine+ module will be isolated from # the application. # - # Consider such controller: + # Consider this controller: # # module MyEngine # class FooController < ActionController::Base # end # end # - # If an engine is marked as isolated, +FooController+ has access only to helpers from +Engine+ and - # <tt>url_helpers</tt> from <tt>MyEngine::Engine.routes</tt>. + # If the +MyEngine+ engine is marked as isolated, +FooController+ only has + # access to helpers from +MyEngine+, and <tt>url_helpers</tt> from + # <tt>MyEngine::Engine.routes</tt>. # - # The next thing that changes in isolated engines is the behavior of routes. Normally, when you namespace - # your controllers, you also need to do namespace all your routes. With an isolated engine, - # the namespace is applied by default, so you can ignore it in routes: + # The next thing that changes in isolated engines is the behavior of routes. + # Normally, when you namespace your controllers, you also need to namespace + # the related routes. With an isolated engine, the engine's namespace is + # automatically applied, so you don't need to specify it explicity in your + # routes: # # MyEngine::Engine.routes.draw do # resources :articles # end # - # The routes above will automatically point to <tt>MyEngine::ArticlesController</tt>. Furthermore, you don't - # need to use longer url helpers like <tt>my_engine_articles_path</tt>. Instead, you should simply use - # <tt>articles_path</tt> as you would do with your application. + # If +MyEngine+ is isolated, The routes above will point to + # <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer + # url helpers like +my_engine_articles_path+. Instead, you should simply use + # +articles_path+, like you would do with your main application. # - # To make that behavior consistent with other parts of the framework, an isolated engine also has influence on - # <tt>ActiveModel::Naming</tt>. When you use a namespaced model, like <tt>MyEngine::Article</tt>, it will normally - # use the prefix "my_engine". In an isolated engine, the prefix will be omitted in url helpers and - # form fields for convenience. + # To make this behavior consistent with other parts of the framework, + # isolated engines also have an effect on <tt>ActiveModel::Naming</tt>. In a + # normal Rails app, when you use a namespaced model such as + # <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate + # names with the prefix "namespace". In an isolated engine, the prefix will + # be omitted in url helpers and form fields, for convenience. # - # polymorphic_url(MyEngine::Article.new) # => "articles_path" + # polymorphic_url(MyEngine::Article.new) + # # => "articles_path" # not "my_engine_articles_path" # # form_for(MyEngine::Article.new) do # text_field :title # => <input type="text" name="article[title]" id="article_title" /> # end # - # Additionally, an isolated engine will set its name according to namespace, so - # MyEngine::Engine.engine_name will be "my_engine". It will also set MyEngine.table_name_prefix - # to "my_engine_", changing the MyEngine::Article model to use the my_engine_articles table. + # Additionally, an isolated engine will set its own name according to its + # namespace, so <tt>MyEngine::Engine.engine_name</tt> will return + # "my_engine". It will also set +MyEngine.table_name_prefix+ to "my_engine_", + # meaning for example that <tt>MyEngine::Article</tt> will use the + # +my_engine_articles+ database table by default. # # == Using Engine's routes outside Engine # @@ -296,7 +303,7 @@ module Rails # helper MyEngine::SharedEngineHelper # end # - # If you want to include all of the engine's helpers, you can use #helper method on an engine's + # If you want to include all of the engine's helpers, you can use the #helper method on an engine's # instance: # # class ApplicationController < ActionController::Base @@ -312,7 +319,7 @@ module Rails # Engines can have their own migrations. The default path for migrations is exactly the same # as in application: <tt>db/migrate</tt> # - # To use engine's migrations in application you can use rake task, which copies them to + # To use engine's migrations in application you can use the rake task below, which copies them to # application's dir: # # rake ENGINE_NAME:install:migrations @@ -328,7 +335,7 @@ module Rails # # == Loading priority # - # In order to change engine's priority you can use +config.railties_order+ in main application. + # In order to change engine's priority you can use +config.railties_order+ in the main application. # It will affect the priority of loading views, helpers, assets and all the other files # related to engine or application. # @@ -405,7 +412,7 @@ module Rails end end - # Finds engine with given path + # Finds engine with given path. def find(path) expanded_path = File.expand_path path Rails::Engine.subclasses.each do |klass| @@ -484,7 +491,7 @@ module Rails helpers = Module.new all = ActionController::Base.all_helpers_from_path(helpers_paths) ActionController::Base.modules_for_helpers(all).each do |mod| - helpers.send(:include, mod) + helpers.include(mod) end helpers end @@ -513,7 +520,7 @@ module Rails def call(env) env.merge!(env_config) if env['SCRIPT_NAME'] - env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup + env[routes.env_key] = env['SCRIPT_NAME'].dup end app.call(env) end @@ -528,7 +535,7 @@ module Rails # Defines the routes for this engine. If a block is given to # routes, it is appended to the engine. def routes - @routes ||= ActionDispatch::Routing::RouteSet.new + @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config) @routes.append(&Proc.new) if block_given? @routes end @@ -559,7 +566,7 @@ module Rails # and the load_once paths. # # This needs to be an initializer, since it needs to run once - # per engine and get the engine as a block parameter + # per engine and get the engine as a block parameter. initializer :set_autoload_paths, before: :bootstrap_hook do ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths) @@ -599,12 +606,6 @@ module Rails end end - initializer :append_assets_path, group: :all do |app| - app.config.assets.paths.unshift(*paths["vendor/assets"].existent_directories) - app.config.assets.paths.unshift(*paths["lib/assets"].existent_directories) - app.config.assets.paths.unshift(*paths["app/assets"].existent_directories) - end - initializer :prepend_helpers_path do |app| if !isolated? || (app == self) app.config.helpers_paths.unshift(*paths["app/helpers"].existent) diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index e25c364178..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 @@ -156,10 +175,10 @@ module Rails args << "--help" if args.empty? && klass.arguments.any?(&:required?) klass.start(args, config) else - options = sorted_groups.map(&:last).flatten + options = sorted_groups.flat_map(&:last) suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) msg = "Could not find generator '#{namespace}'. " - msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n" + msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ") }\n" msg << "Run `rails generate --help` for more options." puts msg end @@ -260,19 +279,20 @@ module Rails t = str2 n = s.length m = t.length - max = n/2 return m if (0 == n) return n if (0 == m) - return n if (n - m).abs > max d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion @@ -286,7 +306,7 @@ module Rails d[m] = x end - return x + x end # Prints a list of generators. diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 5373121835..70a20801a0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -1,5 +1,4 @@ require 'open-uri' -require 'rbconfig' module Rails module Generators @@ -219,10 +218,10 @@ module Rails # route "root 'welcome#index'" def route(routing_code) log :route, routing_code - sentinel = /\.routes\.draw do\s*$/ + sentinel = /\.routes\.draw do\s*\n/m in_root do - inject_into_file 'config/routes.rb', "\n #{routing_code}", { after: sentinel, verbose: false } + inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: true } end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 71186891a3..249fe96772 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -69,8 +69,8 @@ module Rails class_option :skip_turbolinks, type: :boolean, default: false, desc: 'Skip turbolinks gem' - class_option :skip_test_unit, type: :boolean, aliases: '-T', default: false, - desc: 'Skip Test::Unit files' + class_option :skip_test, type: :boolean, aliases: '-T', default: false, + desc: 'Skip test files' class_option :rc, type: :string, default: false, desc: "Path to file containing extra configuration options for rails command" @@ -113,7 +113,6 @@ module Rails assets_gemfile_entry, javascript_gemfile_entry, jbuilder_gemfile_entry, - sdoc_gemfile_entry, psych_gemfile_entry, @extra_entries].flatten.find_all(&@gem_filter) end @@ -127,7 +126,7 @@ module Rails def builder @builder ||= begin builder_class = get_builder_class - builder_class.send(:include, ActionMethods) + builder_class.include(ActionMethods) builder_class.new(self) end end @@ -168,13 +167,17 @@ module Rails end def include_all_railties? - options.values_at(:skip_active_record, :skip_action_mailer, :skip_test_unit, :skip_sprockets).none? + options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets).none? end def comment_if(value) options[value] ? '# ' : '' end + def keeps? + !options[:skip_keeps] + end + def sqlite3? !options[:skip_active_record] && options[:database] == 'sqlite3' end @@ -205,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 @@ -261,15 +266,12 @@ 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 - def sdoc_gemfile_entry - comment = 'bundle exec rake doc:rails generates the API under doc/api.' - GemfileEntry.new('sdoc', '~> 0.4.0', comment, group: :doc) - end - def coffee_gemfile_entry comment = 'Use CoffeeScript for .coffee assets and views' if options.dev? || options.edge? @@ -297,7 +299,7 @@ module Rails end def javascript_runtime_gemfile_entry - comment = 'See https://github.com/sstephenson/execjs#readme for more supported runtimes' + comment = 'See https://github.com/rails/execjs#readme for more supported runtimes' if defined?(JRUBY_VERSION) GemfileEntry.version 'therubyrhino', nil, comment else @@ -321,10 +323,6 @@ module Rails # its own vendored Thor, which could be a different version. Running both # things in the same process is a recipe for a night with paracetamol. # - # We use backticks and #print here instead of vanilla #system because it - # is easier to silence stdout in the existing test suite this way. The - # end-user gets the bundler commands called anyway, so no big deal. - # # We unset temporary bundler variables to load proper bundler and Gemfile. # # Thanks to James Tucker for the Gem tricks involved in this call. @@ -332,8 +330,12 @@ module Rails require 'bundler' Bundler.with_clean_env do - output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}` - print output unless options[:quiet] + full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] + if options[:quiet] + system(full_command, out: File::NULL) + else + system(full_command) + end end end @@ -342,7 +344,7 @@ module Rails end def spring_install? - !options[:skip_spring] && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") + !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end def run_bundle @@ -361,7 +363,7 @@ module Rails end def keep_file(destination) - create_file("#{destination}/.keep") unless options[:skip_keeps] + create_file("#{destination}/.keep") if keeps? end end end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 7d27321610..65563aa6db 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -6,7 +6,7 @@ module Erb # :nodoc: argument :actions, type: :array, default: [], banner: "method method" def copy_view_files - view_base_path = File.join("app/views", class_path, file_name) + view_base_path = File.join("app/views", class_path, file_name + '_mailer') empty_directory view_base_path if self.behavior == :invoke @@ -31,6 +31,10 @@ module Erb # :nodoc: def formats [:text, :html] end + + def file_name + @_file_name ||= super.gsub(/\_mailer/i, '') + end end end end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index d9713b0238..d99b27cb2d 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -17,6 +17,7 @@ <%%= f.label :password %><br> <%%= f.password_field :password %> </div> + <div class="field"> <%%= f.label :password_confirmation %><br> <%%= f.password_field :password_confirmation %> @@ -25,6 +26,7 @@ <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> + <% end -%> <div class="actions"> <%%= f.submit %> 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 5e194783ff..c3b8ef1181 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -1,6 +1,6 @@ <p id="notice"><%%= notice %></p> -<h1>Listing <%= plural_table_name.titleize %></h1> +<h1><%= plural_table_name.titleize %></h1> <table> <thead> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 77a3dbf9a2..8145a26e22 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -142,7 +142,11 @@ module Rails end def password_digest? - name == 'password' && type == :digest + name == 'password' && type == :digest + end + + def token? + type == :token end def inject_options diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index cd388e590a..51e6d68bf0 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -3,8 +3,8 @@ require 'rails/generators/actions/create_migration' module Rails module Generators - # Holds common methods for migrations. It assumes that migrations has the - # [0-9]*_name format and can be used by another frameworks (like Sequel) + # Holds common methods for migrations. It assumes that migrations have the + # [0-9]*_name format and can be used by other frameworks (like Sequel) # just by implementing the next migration version method. module Migration extend ActiveSupport::Concern diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 36456e64f5..01a8e2e9b4 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -141,6 +141,10 @@ module Rails @plural_file_name ||= file_name.pluralize end + def fixture_file_name + @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) + end + def route_url @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 0550bf113e..4b73313388 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -38,7 +38,7 @@ module Rails end def readme - copy_file "README.rdoc", "README.rdoc" + copy_file "README.md", "README.md" end def gemfile @@ -89,6 +89,7 @@ module Rails def config_when_updating cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb') callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb') + active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb') config @@ -99,6 +100,10 @@ module Rails unless cookie_serializer_config_exist gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal' end + + unless active_record_belongs_to_required_by_default_config_exist + remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' + end end def database_yml @@ -125,6 +130,7 @@ module Rails def test empty_directory_with_keep_file 'test/fixtures' + empty_directory_with_keep_file 'test/fixtures/files' empty_directory_with_keep_file 'test/controllers' empty_directory_with_keep_file 'test/mailers' empty_directory_with_keep_file 'test/models' @@ -135,6 +141,7 @@ module Rails end def tmp + empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" empty_directory "tmp/cache/assets" end @@ -168,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 @@ -178,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! @@ -234,7 +248,7 @@ module Rails end def create_test_files - build(:test) unless options[:skip_test_unit] + build(:test) unless options[:skip_test] end def create_tmp_files @@ -245,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' @@ -257,6 +293,19 @@ module Rails end end + def delete_active_record_initializers_skipping_active_record + if options[:skip_active_record] + remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' + 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 @@ -307,7 +356,7 @@ 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." + raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" 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 3659edcfcd..606f1d4f96 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -21,11 +21,21 @@ source 'https://rubygems.org' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development -group :development, :test do +<%- if options.api? -%> +# Use ActiveModelSerializers to serialize JSON responses +gem 'active_model_serializers', '~> 0.10.0.rc1' + +# 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 gem 'byebug' +end +group :development do # 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" @@ -36,10 +46,10 @@ group :development, :test do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' <% end -%> -<% end -%> end - +<% end -%> <% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] <% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/README.rdoc b/railties/lib/rails/generators/rails/app/templates/README.md index dd4e97e22e..55e144da18 100644 --- a/railties/lib/rails/generators/rails/app/templates/README.rdoc +++ b/railties/lib/rails/generators/rails/app/templates/README.md @@ -1,4 +1,4 @@ -== README +## README This README would normally document whatever steps are necessary to get the application up and running. @@ -22,7 +22,3 @@ Things you may want to cover: * Deployment instructions * ... - - -Please feel free to use a different markup language if you do not plan to run -<tt>rake doc:app</tt>. 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 c1a77944e8..cb86978d4c 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 @@ -7,7 +7,7 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // <% unless options[:skip_javascript] -%> 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 f9cd5b3483..0cdd2788d0 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 @@ -6,9 +6,8 @@ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any styles - * defined in the other CSS/SCSS files in this directory. It is generally better to create a new - * file per style scope. + * 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. * *= 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/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup index eee810be30..0d41f2fe4c 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -22,9 +22,8 @@ chdir APP_ROOT do system 'ruby bin/rake db:setup' puts "\n== Removing old logs and tempfiles ==" - rm_f Dir.glob('log/*') - rm_rf 'tmp/cache' + 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/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index c59ffb3491..6b7d7abd0b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -3,6 +3,7 @@ require File.expand_path('../boot', __FILE__) <% if include_all_railties? -%> require 'rails/all' <% else -%> +require "rails" # Pick the frameworks you want: require "active_model/railtie" require "active_job/railtie" @@ -11,7 +12,7 @@ require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" <%= comment_if :skip_sprockets %>require "sprockets/railtie" -<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie" +<%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> # Require the gems listed in Gemfile, including any gems @@ -31,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/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index acb93939e1..f5b62e8fb3 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -7,7 +7,7 @@ # gem 'activerecord-jdbcmysql-adapter' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +# http://dev.mysql.com/doc/refman/5.6/en/old-client.html # default: &default adapter: mysql diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index 596c916573..b0767bd93a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -7,7 +7,7 @@ # gem 'mysql2' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +# http://dev.mysql.com/doc/refman/5.6/en/old-client.html # default: &default adapter: mysql2 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 99d7bfb3c9..8c09396fc1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -14,12 +14,6 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like - # NGINX, varnish or squid. - # config.action_dispatch.rack_cache = true - # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? @@ -39,6 +33,9 @@ Rails.application.configure do # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb <%- end -%> + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX @@ -51,7 +48,7 @@ Rails.application.configure do config.log_level = :debug # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] + # config.log_tags = [ :subdomain, :request_id ] # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) @@ -59,8 +56,9 @@ Rails.application.configure do # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" <%- unless options.skip_action_mailer? -%> # Ignore bad email addresses and do not raise email delivery errors. diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb new file mode 100644 index 0000000000..30c4f89792 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Require `belongs_to` associations by default. +Rails.application.config.active_record.belongs_to_required_by_default = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..ea930f54da --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +## Change renderer defaults here. +# +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb index e63022da91..a70a1b9cde 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Do not halt callback chains when a callback returns false. -Rails.application.config.active_support.halt_callback_chains_on_return_false = false +ActiveSupport.halt_callback_chains_on_return_false = false 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 f2110c2c70..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,12 +5,12 @@ # 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? -%> # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true +# self.include_root_in_json = true # end <%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 7c6f2098b8..1b8cf8a9fa 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -15,5 +15,8 @@ <% end -%> # Ignore all logfiles and tempfiles. /log/* +/tmp/* +<% if keeps? -%> !/log/.keep -/tmp +!/tmp/.keep +<% end -%> diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index df615c88b5..0a4c509a31 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -13,12 +13,14 @@ module Rails def add_routes unless options[:skip_routes] actions.reverse_each do |action| - route generate_routing_code(action) + # route prepends two spaces onto the front of the string that is passed, this corrects that. + route generate_routing_code(action)[2..-1] end end end - hook_for :template_engine, :test_framework, :helper, :assets + hook_for :template_engine, :test_framework + hook_for :helper, :assets, hide: true private @@ -36,12 +38,12 @@ module Rails # namespace :foo do # namespace :bar do namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent("namespace :#{ns} do\n", i * 2) + indent(" namespace :#{ns} do\n", i * 2) end.join # Create route # get 'baz/index' - route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2) + route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2) # Create `end` ladder # end diff --git a/railties/lib/rails/generators/rails/migration/migration_generator.rb b/railties/lib/rails/generators/rails/migration/migration_generator.rb index 965c42db36..fca2a8fef4 100644 --- a/railties/lib/rails/generators/rails/migration/migration_generator.rb +++ b/railties/lib/rails/generators/rails/migration/migration_generator.rb @@ -2,7 +2,7 @@ module Rails module Generators class MigrationGenerator < NamedBase # :nodoc: argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" - hook_for :orm, required: true + hook_for :orm, required: true, desc: "ORM to be invoked" end end end diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 8c3b63c3b4..11daa5c3cb 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -22,7 +22,7 @@ Description: If you pass a namespaced model name (e.g. admin/account or Admin::Account) then the generator will create a module with a table_name_prefix method - to prefix the model's table name with the module name (e.g. admin_account) + to prefix the model's table name with the module name (e.g. admin_accounts) Available field types: @@ -79,10 +79,15 @@ Available field types: `rails generate model product supplier:references{polymorphic}:index` If you require a `password_digest` string column for use with - has_secure_password, you should specify `password:digest`: + has_secure_password, you can specify `password:digest`: `rails generate model user password:digest` + If you require a `token` string column for use with + has_secure_token, you can specify `auth_token:token`: + + `rails generate model user auth_token:token` + Examples: `rails generate model account` diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb index 87bab129bb..ec78fd855d 100644 --- a/railties/lib/rails/generators/rails/model/model_generator.rb +++ b/railties/lib/rails/generators/rails/model/model_generator.rb @@ -6,7 +6,7 @@ module Rails include Rails::Generators::ModelHelpers argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" - hook_for :orm, required: true + hook_for :orm, required: true, desc: "ORM to be invoked" end end end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 1c270dd7d4..7f5bf0c8b8 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -8,7 +8,7 @@ module Rails # generator. # # This allows you to override entire operations, like the creation of the - # Gemfile, README, or JavaScript files, without needing to know exactly + # Gemfile, \README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. class PluginBuilder def rakefile @@ -18,14 +18,14 @@ module Rails def app if mountable? directory 'app' - empty_directory_with_keep_file "app/assets/images/#{name}" + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" 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/#{name}" + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" end end @@ -50,10 +50,10 @@ module Rails end def lib - template "lib/%name%.rb" - template "lib/tasks/%name%_tasks.rake" - template "lib/%name%/version.rb" - template "lib/%name%/engine.rb" if engine? + template "lib/%namespaced_name%.rb" + template "lib/tasks/%namespaced_name%_tasks.rake" + template "lib/%namespaced_name%/version.rb" + template "lib/%namespaced_name%/engine.rb" if engine? end def config @@ -62,7 +62,7 @@ module Rails def test template "test/test_helper.rb" - template "test/%name%_test.rb" + template "test/%namespaced_name%_test.rb" append_file "Rakefile", <<-EOF #{rakefile_test_tasks} @@ -117,9 +117,9 @@ task default: :test def stylesheets if mountable? copy_file "rails/stylesheets.css", - "app/assets/stylesheets/#{name}/application.css" + "app/assets/stylesheets/#{namespaced_name}/application.css" elsif full? - empty_directory_with_keep_file "app/assets/stylesheets/#{name}" + empty_directory_with_keep_file "app/assets/stylesheets/#{namespaced_name}" end end @@ -128,9 +128,9 @@ task default: :test if mountable? template "rails/javascripts.js", - "app/assets/javascripts/#{name}/application.js" + "app/assets/javascripts/#{namespaced_name}/application.js" elsif full? - empty_directory_with_keep_file "app/assets/javascripts/#{name}" + empty_directory_with_keep_file "app/assets/javascripts/#{namespaced_name}" end end @@ -226,7 +226,7 @@ task default: :test end def create_test_files - build(:test) unless options[:skip_test_unit] + build(:test) unless options[:skip_test] end def create_test_dummy_files @@ -256,6 +256,14 @@ task default: :test end end + def underscored_name + @underscored_name ||= original_name.underscore + end + + def namespaced_name + @namespaced_name ||= name.gsub('-', '/') + end + protected def app_templates_dir @@ -294,7 +302,7 @@ task default: :test end def with_dummy_app? - options[:skip_test_unit].blank? || options[:dummy_path] != 'test/dummy' + options[:skip_test].blank? || options[:dummy_path] != 'test/dummy' end def self.banner @@ -305,6 +313,27 @@ task default: :test @original_name ||= File.basename(destination_root) end + def modules + @modules ||= namespaced_name.camelize.split("::") + end + + def wrap_in_modules(unwrapped_code) + unwrapped_code = "#{unwrapped_code}".strip.gsub(/\W$\n/, '') + modules.reverse.inject(unwrapped_code) do |content, mod| + str = "module #{mod}\n" + str += content.lines.map { |line| " #{line}" }.join + str += content.present? ? "\nend" : "end" + end + end + + def camelized_modules + @camelized_modules ||= namespaced_name.camelize + end + + def humanized + @humanized ||= original_name.underscore.humanize + end + def camelized @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize end @@ -328,12 +357,14 @@ task default: :test end def valid_const? - if original_name =~ /[^0-9a-zA-Z_]+/ - raise Error, "Invalid plugin name #{original_name}. Please give a name which use only alphabetic or numeric or \"_\" characters." + if original_name =~ /-\d/ + raise Error, "Invalid plugin name #{original_name}. Please give a name which does not contain a namespace starting with numeric characters." + elsif original_name =~ /[^\w-]+/ + raise Error, "Invalid plugin name #{original_name}. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters." 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." + raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" 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/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index 919c349470..f8ece4fe73 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -1,21 +1,21 @@ $:.push File.expand_path("../lib", __FILE__) # Maintain your gem's version: -require "<%= name %>/version" +require "<%= namespaced_name %>/version" # Describe your gem and declare its dependencies: Gem::Specification.new do |s| s.name = "<%= name %>" - s.version = <%= camelized %>::VERSION + s.version = <%= camelized_modules %>::VERSION s.authors = ["<%= author %>"] s.email = ["<%= email %>"] s.homepage = "TODO" - s.summary = "TODO: Summary of <%= camelized %>." - s.description = "TODO: Description of <%= camelized %>." + s.summary = "TODO: Summary of <%= camelized_modules %>." + s.description = "TODO: Description of <%= camelized_modules %>." s.license = "MIT" s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] -<% unless options.skip_test_unit? -%> +<% unless options.skip_test? -%> s.test_files = Dir["test/**/*"] <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index f325455bac..2c91c6a0ea 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -41,7 +41,7 @@ end # To use a debugger # gem 'byebug', group: [:development, :test] <% end -%> - <% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> + gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc index 301d647731..25983ca5da 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc +++ b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc @@ -1,3 +1,3 @@ -= <%= camelized %> += <%= camelized_modules %> This project rocks and uses MIT-LICENSE.
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index c338a0bdb1..bda55bae29 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -8,7 +8,7 @@ require 'rdoc/task' RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' - rdoc.title = '<%= camelized %>' + rdoc.title = '<%= camelized_modules %>' rdoc.options << '--line-numbers' rdoc.rdoc_files.include('README.rdoc') rdoc.rdoc_files.include('lib/**/*.rb') diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt index 448ad7f989..7157e48c42 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt @@ -1,4 +1,5 @@ -module <%= camelized %> +<%= wrap_in_modules <<-rb.strip_heredoc class ApplicationController < ActionController::Base end -end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt deleted file mode 100644 index 40ae9f52c2..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt +++ /dev/null @@ -1,4 +0,0 @@ -module <%= camelized %> - module ApplicationHelper - end -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt new file mode 100644 index 0000000000..25d692732d --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt @@ -0,0 +1,5 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + module ApplicationHelper + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt deleted file mode 100644 index 1d380420b4..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title><%= camelized %></title> - <%%= stylesheet_link_tag "<%= name %>/application", media: "all" %> - <%%= javascript_include_tag "<%= name %>/application" %> - <%%= csrf_meta_tags %> -</head> -<body> - -<%%= yield %> - -</body> -</html> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt new file mode 100644 index 0000000000..6bc480161d --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> + <title><%= humanized %></title> + <%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %> + <%%= javascript_include_tag "<%= namespaced_name %>/application" %> + <%%= csrf_meta_tags %> +</head> +<body> + +<%%= yield %> + +</body> +</html> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index c3314d7e68..3edaac35c9 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -1,7 +1,7 @@ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/<%= name -%>/engine', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb index 8e158d5831..154452bfe5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb @@ -1,5 +1,5 @@ <% if mountable? -%> -<%= camelized %>::Engine.routes.draw do +<%= camelized_modules %>::Engine.routes.draw do <% else -%> Rails.application.routes.draw do <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore index 086d87818a..d524fcbc4e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore @@ -1,10 +1,10 @@ .bundle/ log/*.log pkg/ -<% unless options[:skip_test_unit] && options[:dummy_path] == 'test/dummy' -%> +<% unless options[:skip_test] && options[:dummy_path] == 'test/dummy' -%> <%= dummy_path %>/db/*.sqlite3 <%= dummy_path %>/db/*.sqlite3-journal <%= dummy_path %>/log/*.log <%= dummy_path %>/tmp/ <%= dummy_path %>/.sass-cache -<% end -%>
\ No newline at end of file +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb deleted file mode 100644 index 40c074cced..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb +++ /dev/null @@ -1,6 +0,0 @@ -<% if engine? -%> -require "<%= name %>/engine" - -<% end -%> -module <%= camelized %> -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb deleted file mode 100644 index 967668fe66..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb +++ /dev/null @@ -1,7 +0,0 @@ -module <%= camelized %> - class Engine < ::Rails::Engine -<% if mountable? -%> - isolate_namespace <%= camelized %> -<% end -%> - end -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb deleted file mode 100644 index ef07ef2e19..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module <%= camelized %> - VERSION = "0.0.1" -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb new file mode 100644 index 0000000000..40b1c4cee7 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb @@ -0,0 +1,5 @@ +<% if engine? -%> +require "<%= namespaced_name %>/engine" + +<% end -%> +<%= wrap_in_modules "# Your code goes here..." %> 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 new file mode 100644 index 0000000000..17afd52177 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb @@ -0,0 +1,6 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + class Engine < ::Rails::Engine + #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '} + 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 new file mode 100644 index 0000000000..d257295988 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb @@ -0,0 +1 @@ +<%= wrap_in_modules 'VERSION = "0.0.1"' %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake index 7121f5ae23..88a2c4120f 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake @@ -1,4 +1,4 @@ # desc "Explaining what the task does" -# task :<%= name %> do +# task :<%= underscored_name %> do # # Task goes here # end diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb index 3a9a7e5437..b1038c839e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb @@ -9,10 +9,10 @@ require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" <%= comment_if :skip_sprockets %>require "sprockets/railtie" -<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie" +<%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> Bundler.require(*Rails.groups) -require "<%= name %>" +require "<%= namespaced_name %>" <%= application_definition %> 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 c28e5badc6..8913b40f69 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js @@ -7,7 +7,7 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require_tree . diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb index 730ee31c3d..673de44108 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb @@ -1,4 +1,4 @@ Rails.application.routes.draw do - mount <%= camelized %>::Engine => "/<%= name %>" + mount <%= camelized_modules %>::Engine => "/<%= name %>" end 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 f9cd5b3483..0cdd2788d0 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css @@ -6,9 +6,8 @@ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any styles - * defined in the other CSS/SCSS files in this directory. It is generally better to create a new - * file per style scope. + * 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. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb deleted file mode 100644 index 0a8bbd4aaf..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class <%= camelized %>Test < ActiveSupport::TestCase - test "truth" do - assert_kind_of Module, <%= camelized %> - end -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb new file mode 100644 index 0000000000..1ee05d7871 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class <%= camelized_modules %>::Test < ActiveSupport::TestCase + test "truth" do + assert_kind_of Module, <%= camelized_modules %> + end +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 bf3da1fc4d..0852ffce9a 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,4 +20,6 @@ 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" + ActiveSupport::TestCase.fixtures :all end diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 8014feb75f..3acf21df13 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -1,6 +1,5 @@ require 'rails/generators/resource_helpers' require 'rails/generators/rails/model/model_generator' -require 'active_support/core_ext/object/blank' module Rails module Generators 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 e4a2bc2b0f..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 @@ -29,8 +28,10 @@ module Rails write("end", route_length - index) end - # route prepends two spaces onto the front of the string that is passed, this corrects that - route route_string[2..-1] + # route prepends two spaces onto the front of the string that is passed, this corrects that. + # Also it adds a \n to the end of each line, as route already adds that + # we need to correct that too. + route route_string[2..-2] end private diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index 1b2a944103..d2e495758d 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -36,6 +36,6 @@ Description: Examples: `rails generate scaffold post` - `rails generate scaffold post title body:text published:boolean` + `rails generate scaffold post title:string body:text published:boolean` `rails generate scaffold purchase amount:decimal tracking_id:integer:uniq` `rails generate scaffold user email:uniq password:digest` 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/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 1ae7000299..b7818883d1 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -1,9 +1,13 @@ -body { background-color: #fff; color: #333; } +body { + background-color: #fff; + color: #333; +} body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; + font-size: 13px; line-height: 18px; + margin: 33px; } pre { @@ -12,11 +16,31 @@ pre { font-size: 11px; } -a { color: #000; } -a:visited { color: #666; } -a:hover { color: #fff; background-color:#000; } +a { + color: #000; +} + +a:visited { + color: #666; +} + +a:hover { + color: #fff; + background-color: #000; +} + +th { + padding-bottom: 5px; +} + +td { + padding-bottom: 7px; + padding-left: 5px; + padding-right: 5px; +} -div.field, div.actions { +div.field, +div.actions { margin-bottom: 10px; } @@ -45,7 +69,7 @@ div.field, div.actions { padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; - margin-bottom: 0px; + margin-bottom: 0; background-color: #c00; color: #fff; } 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/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 3f84d76ae0..9c2037783e 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -8,7 +8,7 @@ module Rails module ResourceHelpers # :nodoc: def self.included(base) #:nodoc: - base.send :include, Rails::Generators::ModelHelpers + base.include(Rails::Generators::ModelHelpers) base.class_option :model_name, type: :string, desc: "ModelName to be used" 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..7106096b60 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 defined?(ENGINE_ROOT) -%> + 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/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 85dee1a066..343c8a3949 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -6,16 +6,21 @@ module TestUnit # :nodoc: argument :actions, type: :array, default: [], banner: "method method" def check_class_collision - class_collisions "#{class_name}Test", "#{class_name}Preview" + class_collisions "#{class_name}MailerTest", "#{class_name}MailerPreview" end def create_test_files - template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb") + template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_mailer_test.rb") end def create_preview_files - template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb") + template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_mailer_preview.rb") end + + protected + def file_name + @_file_name ||= super.gsub(/\_mailer/i, '') + end end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb index 7e204105a3..a2f2d30de5 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb @@ -1,10 +1,10 @@ require 'test_helper' <% module_namespacing do -%> -class <%= class_name %>Test < ActionMailer::TestCase +class <%= class_name %>MailerTest < ActionMailer::TestCase <% actions.each do |action| -%> test "<%= action %>" do - mail = <%= class_name %>.<%= action %> + mail = <%= class_name %>Mailer.<%= action %> assert_equal <%= action.to_s.humanize.inspect %>, mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb index 3bfd5426e8..b063cbc47b 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb @@ -1,11 +1,11 @@ <% module_namespacing do -%> -# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %> -class <%= class_name %>Preview < ActionMailer::Preview +# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %>_mailer +class <%= class_name %>MailerPreview < ActionMailer::Preview <% actions.each do |action| -%> - # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> + # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>_mailer/<%= action %> def <%= action %> - <%= class_name %>.<%= action %> + <%= class_name %>Mailer.<%= action %> end <% end -%> diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 2826a3ffa1..086588750e 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -19,7 +19,7 @@ module TestUnit # :nodoc: def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml") + template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml") end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 2e1f55f2a6..d634584beb 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 defined?(ENGINE_ROOT) + "%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..896b38bc8f --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +<% module_namespacing do -%> +class <%= controller_class_name %>ControllerTest < ActionController::TestCase + setup do + @<%= singular_table_name %> = <%= table_name %>(:one) + 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 18bd1ece9d..d12a5ba733 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,13 +3,15 @@ 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 defined?(ENGINE_ROOT) -%> + @routes = Engine.routes +<% end -%> end test "should get index" do get :index assert_response :success - assert_not_nil assigns(:<%= table_name %>) end test "should get new" do @@ -19,30 +21,30 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase test "should create <%= singular_table_name %>" do assert_difference('<%= class_name %>.count') do - post :create, <%= "#{singular_table_name}: { #{attributes_hash} }" %> + post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } end - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last) end test "should show <%= singular_table_name %>" do - get :show, id: <%= "@#{singular_table_name}" %> + get :show, params: { id: <%= "@#{singular_table_name}" %> } assert_response :success end test "should get edit" do - get :edit, id: <%= "@#{singular_table_name}" %> + get :edit, params: { id: <%= "@#{singular_table_name}" %> } assert_response :success end test "should update <%= singular_table_name %>" do - patch :update, id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>) end test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete :destroy, id: <%= "@#{singular_table_name}" %> + delete :destroy, params: { id: <%= "@#{singular_table_name}" %> } end assert_redirected_to <%= index_helper %>_path diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index fd2ea274e1..c9700e1cd7 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/kernel/reporting' +require 'active_support/testing/stream' require 'active_support/concern' require 'rails/generators' @@ -10,6 +11,7 @@ module Rails module Testing module Behaviour extend ActiveSupport::Concern + include ActiveSupport::Testing::Stream included do class_attribute :destination_root, :current_path, :generator_class, :default_arguments @@ -101,22 +103,6 @@ module Rails Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first end - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end end end end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 49e5431a16..778105c5f7 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -17,7 +17,28 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def routes - @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) - @page_title = 'Routes' + if path = params[:path] + path = URI.parser.escape path + normalized_path = with_leading_slash path + render json: { + exact: match_route {|it| it.match normalized_path }, + fuzzy: match_route {|it| it.spec.to_s.match path } + } + else + @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) + @page_title = 'Routes' + end + end + + private + + def match_route + _routes.routes.select {|route| + yield route.path + }.map {|route| route.path.spec.to_s } + end + + def with_leading_slash(path) + ('/' + path).squeeze('/') end end diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 32740d66da..41422a656c 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -16,10 +16,10 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @page_title = "Mailer Previews for #{@preview.preview_name}" render action: 'mailer' else - email = File.basename(params[:path]) + @email_action = File.basename(params[:path]) - if @preview.email_exists?(email) - @email = @preview.call(email) + if @preview.email_exists?(@email_action) + @email = @preview.call(@email_action) if params[:part] part_type = Mime::Type.lookup(params[:part]) @@ -28,14 +28,14 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: response.content_type = part_type render text: part.respond_to?(:decoded) ? part.decoded : part else - raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}" + raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end else @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) render action: 'email', layout: false, formats: %w[html] end else - raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}" + raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" end end end @@ -54,18 +54,20 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end def find_preferred_part(*formats) - if @email.multipart? - formats.each do |format| - return find_part(format) if @email.parts.any?{ |p| p.mime_type == format } + formats.each do |format| + if part = @email.find_first_mime_type(format) + return part end - else + end + + if formats.any?{ |f| @email.mime_type == f } @email end end def find_part(format) - if @email.multipart? - @email.parts.find{ |p| p.mime_type == format } + if part = @email.find_first_mime_type(format) + part elsif @email.mime_type == format @email end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 5458036219..ebcaaaba46 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -7,7 +7,7 @@ module Rails # root = Root.new "/rails" # root.add "app/controllers", eager_load: true # - # The command above creates a new root object and add "app/controllers" as a path. + # The command above creates a new root object and adds "app/controllers" as a path. # This means we can get a <tt>Rails::Paths::Path</tt> object back like below: # # path = root["app/controllers"] diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 9962e6d943..12676b18bc 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -7,6 +7,10 @@ require 'rack/body_proxy' module Rails module Rack # Sets log tags, logs the request, calls the app, and flushes the logs. + # + # Log tags (+taggers+) can be an Array containing: methods that the +request+ + # object responds to, objects that respond to +to_s+ or Proc objects that accept + # an instance of the +request+ object. class Logger < ActiveSupport::LogSubscriber def initialize(app, taggers = nil) @app = app diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 2b33beaa2b..8c24d1d56d 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -93,7 +93,7 @@ module Rails # end # end # - # By default, Rails load generators from your load path. However, if you want to place + # By default, Rails loads generators from your load path. However, if you want to place # your generators at a different location, you can specify in your Railtie a block which # will load them during normal generators lookup: # diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index aea3d2339c..67a19d8a94 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,13 +1,13 @@ -if RUBY_VERSION < '2.2.0' +if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 5 requires to run on Ruby 2.2.0 or newer. + Rails 5 requires Ruby 2.2.2 or newer. You're running #{desc} - Please upgrade to Ruby 2.2.0 or newer to continue. + Please upgrade to Ruby 2.2.2 or newer to continue. end_message end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 2f82d1285d..2c3d278eca 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,11 +3,12 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations - documentation framework + initializers log middleware misc + restart routes statistics tmp diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake deleted file mode 100644 index 8544890553..0000000000 --- a/railties/lib/rails/tasks/documentation.rake +++ /dev/null @@ -1,70 +0,0 @@ -begin - require 'rdoc/task' -rescue LoadError - # Rubinius installs RDoc as a gem, and for this interpreter "rdoc/task" is - # available only if the application bundle includes "rdoc" (normally as a - # dependency of the "sdoc" gem.) - # - # If RDoc is not available it is fine that we do not generate the tasks that - # depend on it. Just be robust to this gotcha and go on. -else - require 'rails/api/task' - - # Monkey-patch to remove redoc'ing and clobber descriptions to cut down on rake -T noise - class RDocTaskWithoutDescriptions < RDoc::Task - include ::Rake::DSL - - def define - task rdoc_task_name - - task rerdoc_task_name => [clobber_task_name, rdoc_task_name] - - task clobber_task_name do - rm_r rdoc_dir rescue nil - end - - task :clobber => [clobber_task_name] - - directory @rdoc_dir - task rdoc_task_name => [rdoc_target] - file rdoc_target => @rdoc_files + [Rake.application.rakefile] do - rm_r @rdoc_dir rescue nil - @before_running_rdoc.call if @before_running_rdoc - args = option_list + @rdoc_files - if @external - argstring = args.join(' ') - sh %{ruby -Ivendor vendor/rd #{argstring}} - else - require 'rdoc/rdoc' - RDoc::RDoc.new.document(args) - end - end - self - end - end - - namespace :doc do - RDocTaskWithoutDescriptions.new("app") { |rdoc| - rdoc.rdoc_dir = 'doc/app' - rdoc.template = ENV['template'] if ENV['template'] - rdoc.title = ENV['title'] || "Rails Application Documentation" - rdoc.options << '--line-numbers' - rdoc.options << '--charset' << 'utf-8' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('app/**/*.rb') - rdoc.rdoc_files.include('lib/**/*.rb') - } - Rake::Task['doc:app'].comment = "Generate docs for the app -- also available doc:rails, doc:guides (options: TEMPLATE=/rdoc-template.rb, TITLE=\"Custom Title\")" - - # desc 'Generate documentation for the Rails framework.' - Rails::API::AppTask.new('rails') - end -end - -namespace :doc do - task :guides do - rails_gem_dir = Gem::Specification.find_by_name("rails").gem_dir - require File.expand_path(File.join(rails_gem_dir, "/guides/rails_guides")) - RailsGuides::Generator.new(Rails.root.join("doc/guides")).generate - end -end diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index a1c805f8aa..904b9d9ad6 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -32,35 +32,37 @@ namespace :rails do FileUtils.cp_r src_name, dst_name end end - end + end end namespace :update do - def invoke_from_app_generator(method) - app_generator.send(method) - end + class RailsUpdate + def self.invoke_from_app_generator(method) + app_generator.send(method) + end - def app_generator - @app_generator ||= begin - require 'rails/generators' - require 'rails/generators/rails/app/app_generator' - gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: Rails.root - File.exist?(Rails.root.join("config", "application.rb")) ? - gen.send(:app_const) : gen.send(:valid_const?) - gen + def self.app_generator + @app_generator ||= begin + require 'rails/generators' + require 'rails/generators/rails/app/app_generator' + gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, + destination_root: Rails.root + File.exist?(Rails.root.join("config", "application.rb")) ? + gen.send(:app_const) : gen.send(:valid_const?) + gen + end end end # desc "Update config/boot.rb from your current rails install" task :configs do - invoke_from_app_generator :create_boot_file - invoke_from_app_generator :update_config_files + RailsUpdate.invoke_from_app_generator :create_boot_file + RailsUpdate.invoke_from_app_generator :update_config_files end # desc "Adds new executables to the application bin/ directory" task :bin do - invoke_from_app_generator :create_bin_files + RailsUpdate.invoke_from_app_generator :create_bin_files end end end diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake new file mode 100644 index 0000000000..2968b5cb53 --- /dev/null +++ b/railties/lib/rails/tasks/initializers.rake @@ -0,0 +1,6 @@ +desc "Print out all defined initializers in the order they are invoked by Rails." +task initializers: :environment do + Rails.application.initializers.tsort_each do |initializer| + puts initializer.name + end +end diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake new file mode 100644 index 0000000000..f36c86d81b --- /dev/null +++ b/railties/lib/rails/tasks/restart.rake @@ -0,0 +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/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index ba6168e208..735c36eb3a 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -14,10 +14,8 @@ STATS_DIRECTORIES = [ %w(Helper\ tests test/helpers), %w(Model\ tests test/models), %w(Mailer\ tests test/mailers), - %w(Job\ tests test/jobs), + %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), - %w(Functional\ tests\ (old) test/functional), - %w(Unit\ tests \ (old) test/unit) ].collect do |name, dir| [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] end.select { |name, dir| File.directory?(dir) } diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake index b33ae9862b..9162ef234a 100644 --- a/railties/lib/rails/tasks/tmp.rake +++ b/railties/lib/rails/tasks/tmp.rake @@ -11,7 +11,7 @@ namespace :tmp do tmp_dirs.each { |d| directory d } - desc "Creates tmp directories for sessions, cache, sockets, and pids" + desc "Creates tmp directories for cache, sockets, and pids" task create: tmp_dirs namespace :cache do diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 0b08a01896..bb6e49966d 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -39,6 +39,10 @@ padding: 1px; } + dd:empty:before { + content: "\00a0"; // + } + iframe { border: 0; width: 100%; @@ -84,7 +88,7 @@ <% unless @email.attachments.nil? || @email.attachments.empty? %> <dt>Attachments:</dt> <dd> - <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %> + <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.join(', ') %> </dd> <% end %> @@ -99,7 +103,14 @@ </dl> </header> -<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% if @part && @part.mime_type %> + <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% else %> + <p> + You are trying to preview an email that does not have any content. + This is probably because the <em>mail</em> method has not been called in <em><%= @preview.preview_name %>#<%= @email_action %></em>. + </p> +<% end %> </body> </html> diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb index c4c9757d57..000930c039 100644 --- a/railties/lib/rails/templates/rails/mailers/index.html.erb +++ b/railties/lib/rails/templates/rails/mailers/index.html.erb @@ -1,8 +1,8 @@ <% @previews.each do |preview| %> -<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3> +<h3><%= link_to preview.preview_name.titleize, url_for(controller: "rails/mailers", action: "preview", path: preview.preview_name) %></h3> <ul> <% preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{preview.preview_name}/#{email}") %></li> <% end %> </ul> <% end %> diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb index 607c8d1677..c12ead0f90 100644 --- a/railties/lib/rails/templates/rails/mailers/mailer.html.erb +++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb @@ -1,6 +1,6 @@ <h3><%= @preview.preview_name.titleize %></h3> <ul> <% @preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{@preview.preview_name}/#{email}") %></li> <% end %> </ul> diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index 6726c23fc9..acf04af416 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -18,7 +18,9 @@ color: #000; } - a {color: #03c} + a { + color: #03c; + } a:hover { background-color: #03c; @@ -64,7 +66,10 @@ height: 64px; } - #header h1, #header h2 {margin: 0} + #header h1, + #header h2 { + margin: 0; + } #header h2 { color: #888; @@ -104,7 +109,9 @@ color: #555; } - #about-content td.value {color: #000} + #about-content td.value { + color: #000; + } #about-content ul { padding: 0; diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index c837fadb40..828039dc43 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -2,18 +2,14 @@ # so fixtures aren't loaded into that environment abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production? -require 'active_support/testing/autorun' +require "rails/test_unit/minitest_plugin" require 'active_support/test_case' require 'action_controller' require 'action_controller/test_case' require 'action_dispatch/testing/integration' require 'rails/generators/test_case' -# Config Rails backtrace in tests. -require 'rails/backtrace_cleaner' -if ENV["BACKTRACE"].nil? - Minitest.backtrace_filter = Rails.backtrace_cleaner -end +require 'active_support/testing/autorun' if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! @@ -21,6 +17,7 @@ if defined?(ActiveRecord::Base) class ActiveSupport::TestCase include ActiveRecord::TestFixtures self.fixture_path = "#{Rails.root}/test/fixtures/" + self.file_fixture_path = self.fixture_path + "files" end ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path @@ -31,13 +28,15 @@ if defined?(ActiveRecord::Base) end class ActionController::TestCase - setup do + def before_setup @routes = Rails.application.routes + super end end class ActionDispatch::IntegrationTest - setup do + def before_setup @routes = Rails.application.routes + super end end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb new file mode 100644 index 0000000000..421f032d81 --- /dev/null +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -0,0 +1,51 @@ +require "active_support/core_ext/module/attribute_accessors" +require "rails/test_unit/reporter" +require "rails/test_unit/test_requirer" + +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 + + 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.extensions << 'rails' diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb new file mode 100644 index 0000000000..bfdc9754d4 --- /dev/null +++ b/railties/lib/rails/test_unit/reporter.rb @@ -0,0 +1,26 @@ +require "minitest" + +module Rails + class TestUnitReporter < Minitest::StatisticsReporter + def report + return if results.empty? + io.puts + io.puts "Failed tests:" + io.puts + io.puts aggregated_results + 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 #{relative_path_for(location)}:#{line}" + end.join "\n" + end + + def relative_path_for(file) + file.sub(/^#{Rails.root}\/?/, '') + end + end +end diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb deleted file mode 100644 index 6fa96d2ced..0000000000 --- a/railties/lib/rails/test_unit/sub_test_task.rb +++ /dev/null @@ -1,126 +0,0 @@ -require 'rake/testtask' - -module Rails - class TestTask < Rake::TestTask # :nodoc: all - # A utility class which is used primarily in "rails/test_unit/testing.rake" - # to help define rake tasks corresponding to <tt>rake test</tt>. - # - # This class takes a TestInfo class and defines the appropriate rake task - # based on the information, then invokes it. - class TestCreator # :nodoc: - def initialize(info) - @info = info - end - - def invoke_rake_task - if @info.files.any? - create_and_run_single_test - reset_application_tasks - else - Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke - end - end - - private - - def create_and_run_single_test - Rails::TestTask.new('test:single') { |t| - t.test_files = @info.files - } - ENV['TESTOPTS'] ||= @info.opts - Rake::Task['test:single'].invoke - end - - def reset_application_tasks - Rake.application.top_level_tasks.replace @info.tasks - end - end - - # This is a utility class used by the <tt>TestTask::TestCreator</tt> class. - # This class takes a set of test tasks and checks to see if they correspond - # to test files (or can be transformed into test files). Calling <tt>files</tt> - # provides the set of test files and is used when initializing tests after - # a call to <tt>rake test</tt>. - class TestInfo # :nodoc: - def initialize(tasks) - @tasks = tasks - @files = nil - end - - def files - @files ||= @tasks.map { |task| - [task, translate(task)].find { |file| test_file?(file) } - }.compact - end - - def translate(file) - if file =~ /^app\/(.*)$/ - "test/#{$1.sub(/\.rb$/, '')}_test.rb" - else - "test/#{file}_test.rb" - end - end - - def tasks - @tasks - test_file_tasks - opt_names - end - - def opts - opts = opt_names - if opts.any? - "-n #{opts.join ' '}" - end - end - - private - - def test_file_tasks - @tasks.find_all { |task| - [task, translate(task)].any? { |file| test_file?(file) } - } - end - - def test_file?(file) - file =~ /^test/ && File.file?(file) && !File.directory?(file) - end - - def opt_names - (@tasks - test_file_tasks).reject { |t| task_defined? t } - end - - def task_defined?(task) - Rake::Task.task_defined? task - end - end - - def self.test_creator(tasks) - info = TestInfo.new(tasks) - TestCreator.new(info) - end - - def initialize(name = :test) - super - @libs << "test" # lib *and* test seem like a better default - end - - def define - task @name do - if ENV['TESTOPTS'] - ARGV.replace Shellwords.split ENV['TESTOPTS'] - end - libs = @libs - $LOAD_PATH - $LOAD_PATH.unshift(*libs) - file_list.each { |fl| - FileList[fl].to_a.each { |f| require File.expand_path f } - } - end - end - end - - # Silence the default description to cut down on `rake -T` noise. - class SubTestTask < Rake::TestTask # :nodoc: - def desc(string) - # Ignore the description. - 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 d836c0d6d6..dda492f974 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,11 +1,13 @@ -require 'rake/testtask' -require 'rails/test_unit/sub_test_task' +gem 'minitest' +require 'minitest' +require 'rails/test_unit/minitest_plugin' task default: :test desc "Runs all tests in test folder" task :test do - Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task + $: << "test" + Minitest.run(['test']) end namespace :test do @@ -14,30 +16,30 @@ namespace :test do # If used with Active Record, this task runs before the database schema is synchronized. end - Rails::TestTask.new(:run) do |t| - t.pattern = "test/**/*_test.rb" - end + task :run => %w[test] desc "Run tests quickly, but also reset db" task :db => %w[db:test:prepare test] - Rails::TestTask.new(single: "test:prepare") - ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| - Rails::TestTask.new(name => "test:prepare") do |t| - t.pattern = "test/#{name}/**/*_test.rb" + task name => "test:prepare" do + $: << "test" + Minitest.run(["test/#{name}"]) end end - Rails::TestTask.new(generators: "test:prepare") do |t| - t.pattern = "test/lib/generators/**/*_test.rb" + task :generators => "test:prepare" do + $: << "test" + Minitest.run(["test/lib/generators"]) end - Rails::TestTask.new(units: "test:prepare") do |t| - t.pattern = 'test/{models,helpers,unit}/**/*_test.rb' + task :units => "test:prepare" do + $: << "test" + Minitest.run(["test/models", "test/helpers", "test/unit"]) end - Rails::TestTask.new(functionals: "test:prepare") do |t| - t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb' + task :functionals => "test:prepare" do + $: << "test" + Minitest.run(["test/controllers", "test/mailers", "test/functional"]) end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 7a1c897e3d..afe1603448 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Tools for creating, working with, and running Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -15,10 +15,10 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}'] + s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' - s.bindir = 'bin' + s.bindir = 'exe' s.executables = ['rails'] s.rdoc_options << '--exclude' << '.' @@ -28,6 +28,7 @@ Gem::Specification.new do |s| s.add_dependency 'rake', '>= 0.8.7' s.add_dependency 'thor', '>= 0.18.1', '< 2.0' + s.add_dependency 'method_source' s.add_development_dependency 'actionview', version end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 0749615d03..794d180e5d 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -4,6 +4,7 @@ require File.expand_path("../../../load_paths", __FILE__) require 'stringio' require 'active_support/testing/autorun' +require 'active_support/testing/stream' require 'fileutils' require 'active_support' @@ -28,26 +29,5 @@ def jruby_skip(message = '') end class ActiveSupport::TestCase - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method call. - self.test_order = :sorted - - private - - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end + include ActiveSupport::Testing::Stream end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_loader_test.rb index d4885447e6..5946c8fd4c 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -1,11 +1,11 @@ require 'tmpdir' require 'abstract_unit' -require 'rails/app_rails_loader' +require 'rails/app_loader' -class AppRailsLoaderTest < ActiveSupport::TestCase +class AppLoaderTest < ActiveSupport::TestCase def loader @loader ||= Class.new do - extend Rails::AppRailsLoader + extend Rails::AppLoader def self.exec_arguments @exec_arguments @@ -23,7 +23,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase end def expects_exec(exe) - assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments + assert_equal [Rails::AppLoader::RUBY, exe], loader.exec_arguments end setup do @@ -38,20 +38,20 @@ class AppRailsLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} is not found in the current or parent directories" do def loader.find_executables; end - assert !loader.exec_app_rails + assert !loader.exec_app end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !loader.exec_app_rails + assert !loader.exec_app end ['APP_PATH', 'ENGINE_PATH'].each do |keyword| test "is in a Rails application if #{exe} exists and contains #{keyword}" do write exe, keyword - loader.exec_app_rails + loader.exec_app expects_exec exe end @@ -59,7 +59,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !loader.exec_app_rails + assert !loader.exec_app end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do @@ -68,7 +68,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase Dir.chdir('foo/bar') - loader.exec_app_rails + loader.exec_app expects_exec exe diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 9a571fac3a..acd387256c 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -57,8 +57,8 @@ module ApplicationTests class ::PostsController < ActionController::Base ; end get '/posts?debug_assets=true' - assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) - assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index d58a27403e..1e2a9ba040 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require 'isolation/abstract_unit' require 'rack/test' require 'active_support/json' @@ -192,6 +191,7 @@ module ApplicationTests test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do add_to_config "config.action_controller.perform_caching = false" + add_to_env_config "production", "config.assets.compile = true" ENV["RAILS_ENV"] = "production" require "#{app_path}/config/environment" @@ -205,7 +205,7 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) @@ -218,7 +218,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/x/manifest-*.json"].first + manifest = Dir["#{app_path}/public/x/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) end @@ -230,7 +230,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] @@ -262,7 +262,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.css"] @@ -292,7 +292,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] @@ -438,9 +438,9 @@ module ApplicationTests class ::PostsController < ActionController::Base; end get '/posts', {}, {'HTTPS'=>'off'} - assert_match('src="http://example.com/assets/application.js', last_response.body) + assert_match('src="http://example.com/assets/application.self.js', last_response.body) get '/posts', {}, {'HTTPS'=>'on'} - assert_match('src="https://example.com/assets/application.js', last_response.body) + assert_match('src="https://example.com/assets/application.self.js', last_response.body) end test "asset urls should be protocol-relative if no request is in scope" do @@ -465,17 +465,6 @@ module ApplicationTests assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) end - test "assets:cache:clean should clean cache" do - ENV["RAILS_ENV"] = "production" - precompile! - - quietly do - Dir.chdir(app_path){ `bundle exec rake assets:clobber` } - end - - assert !File.exist?("#{app_path}/tmp/cache/assets") - end - private def app_with_assets_in_view diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb new file mode 100644 index 0000000000..1bdced02e9 --- /dev/null +++ b/railties/test/application/bin_setup_test.rb @@ -0,0 +1,54 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class BinSetupTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + def test_bin_setup + Dir.chdir(app_path) do + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table(:articles) {} + end + RUBY + + list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } + File.write("log/my.log", "zomg!") + + assert_equal '[]', list_tables.call + assert_equal 5, File.size("log/my.log") + assert_not File.exist?("tmp/restart.txt") + `bin/setup 2>&1` + assert_equal 0, File.size("log/my.log") + assert_equal '["articles", "schema_migrations"]', list_tables.call + assert File.exist?("tmp/restart.txt") + end + end + + def test_bin_setup_output + Dir.chdir(app_path) do + app_file 'db/schema.rb', "" + + output = `bin/setup 2>&1` + assert_equal(<<-OUTPUT, output) +== Installing dependencies == +The Gemfile's dependencies are satisfied + +== Preparing database == + +== Removing old logs and tempfiles == + +== Restarting application server == + OUTPUT + end + end + end +end diff --git a/railties/test/application/build_original_fullpath_test.rb b/railties/test/application/build_original_fullpath_test.rb deleted file mode 100644 index 647ffb097a..0000000000 --- a/railties/test/application/build_original_fullpath_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "abstract_unit" - -module ApplicationTests - class BuildOriginalPathTest < ActiveSupport::TestCase - def test_include_original_PATH_info_in_ORIGINAL_FULLPATH - env = { 'PATH_INFO' => '/foo/' } - assert_equal "/foo/", Rails.application.send(:build_original_fullpath, env) - end - - def test_include_SCRIPT_NAME - env = { - 'SCRIPT_NAME' => '/foo', - 'PATH_INFO' => '/bar' - } - - assert_equal "/foo/bar", Rails.application.send(:build_original_fullpath, env) - end - - def test_include_QUERY_STRING - env = { - 'PATH_INFO' => '/foo', - 'QUERY_STRING' => 'bar', - } - assert_equal "/foo?bar", Rails.application.send(:build_original_fullpath, env) - end - end -end diff --git a/railties/test/application/configuration/base_test.rb b/railties/test/application/configuration/base_test.rb deleted file mode 100644 index d6a82b139d..0000000000 --- a/railties/test/application/configuration/base_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'isolation/abstract_unit' -require 'rack/test' -require 'env_helpers' - -module ApplicationTests - module ConfigurationTests - class BaseTest < ActiveSupport::TestCase - def setup - build_app - boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") - end - - def teardown - teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) - end - - private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - - def require_environment - require "#{app_path}/config/environment" - end - end - end -end
\ No newline at end of file diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index f8d22f6d97..28b3b2f2d6 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -1,22 +1,54 @@ -require 'application/configuration/base_test' - -class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest - test 'access custom configuration point' do - add_to_config <<-RUBY - config.x.payment_processing.schedule = :daily - config.x.payment_processing.retries = 3 - config.x.super_debugger = true - config.x.hyper_debugger = false - config.x.nil_debugger = nil - RUBY - require_environment - - x = Rails.configuration.x - assert_equal :daily, x.payment_processing.schedule - assert_equal 3, x.payment_processing.retries - assert_equal true, x.super_debugger - assert_equal false, x.hyper_debugger - assert_equal nil, x.nil_debugger - assert_nil x.i_do_not_exist.zomg +require 'isolation/abstract_unit' + +module ApplicationTests + module ConfigurationTests + class CustomTest < ActiveSupport::TestCase + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + FileUtils.rm_rf(new_app) if File.directory?(new_app) + end + + test 'access custom configuration point' do + add_to_config <<-RUBY + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + config.x.hyper_debugger = false + config.x.nil_debugger = nil + RUBY + require_environment + + x = Rails.configuration.x + assert_equal :daily, x.payment_processing.schedule + assert_equal 3, x.payment_processing.retries + assert_equal true, x.super_debugger + assert_equal false, x.hyper_debugger + assert_equal nil, x.nil_debugger + assert_nil x.i_do_not_exist.zomg + end + + private + def new_app + File.expand_path("#{app_path}/../new_app") + end + + def copy_app + FileUtils.cp_r(app_path, new_app) + end + + def app + @app ||= Rails.application + end + + def require_environment + require "#{app_path}/config/environment" + end + end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 8f5b2d0d68..f677a7c42a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -690,7 +690,7 @@ module ApplicationTests _ = ActionMailer::Base - assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors end test "registers multiple preview interceptors with ActionMailer" do @@ -703,7 +703,20 @@ module ApplicationTests _ = ActionMailer::Base - assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + end + + test "default preview interceptor can be removed" do + app_file 'config/initializers/preview_interceptors.rb', <<-RUBY + ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) + RUBY + + require "#{app_path}/config/environment" + require "mail" + + _ = ActionMailer::Base + + assert_equal [], ActionMailer::Base.preview_interceptors end test "registers observers with ActionMailer" do @@ -732,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..af98e08d0e 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" diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 85066210f3..1027bca2c1 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -210,7 +210,7 @@ class LoadingTest < ActiveSupport::TestCase app_file 'config/routes.rb', <<-RUBY $counter ||= 0 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY @@ -243,7 +243,7 @@ class LoadingTest < ActiveSupport::TestCase $counter ||= 1 $counter *= 2 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 1752a9f3c6..e462d2c15e 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -1,5 +1,7 @@ require 'isolation/abstract_unit' require 'rack/test' +require 'base64' + module ApplicationTests class MailerPreviewsTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation @@ -327,6 +329,32 @@ module ApplicationTests assert_match "Email 'bar' not found in NotifierPreview", last_response.body end + test "mailer preview NullMail" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + # does not call +mail+ + end + end + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_match "You are trying to preview an email that does not have any content.", last_response.body + assert_match "notifier#foo", last_response.body + end + test "mailer preview email part not found" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base @@ -428,6 +456,222 @@ module ApplicationTests assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body end + test "mailer previews create correct links when loaded on a subdirectory" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers", {}, 'SCRIPT_NAME' => '/my_app' + assert_match '<h3><a href="/my_app/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "plain text mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + end + + test "multipart mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + + test "multipart mailer preview with inline attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + <%= image_tag attachments['pixel.png'].url %> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + assert_match %r[src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="], last_response.body + end + + test "multipart mailer preview with attached email" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + message = ::Mail.new do + from 'foo@example.com' + to 'bar@example.com' + subject 'Important Message' + + text_part do + body 'Goodbye, World!' + end + + html_part do + body '<p>Goodbye, World!</p>' + end + end + + attachments['message.eml'] = message.to_s + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + private def build_app super @@ -449,5 +693,9 @@ module ApplicationTests def text_template(name, contents) app_file("app/views/#{name}.text.erb", contents) end + + def image_file(name, contents) + app_file("public/images/#{name}", Base64.strict_decode64(contents), 'wb') + end end end diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index a7472b37f1..4906f9a1e8 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'isolation/abstract_unit' require 'rack/test' diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index eb7885e5b1..25eadfc387 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'isolation/abstract_unit' require 'rack/test' @@ -36,7 +35,7 @@ module ApplicationTests flash[:notice] = "notice" end - render nothing: true + head :ok end end @@ -61,7 +60,7 @@ module ApplicationTests def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -102,7 +101,7 @@ module ApplicationTests def write_cookie cookies[:foo] = '1' - render nothing: true + head :ok end def read_cookie @@ -140,7 +139,7 @@ module ApplicationTests class FooController < ActionController::Base def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -185,7 +184,7 @@ module ApplicationTests class FooController < ActionController::Base def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -235,12 +234,12 @@ module ApplicationTests def write_raw_session # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - render nothing: true + head :ok end def write_session session[:foo] = session[:foo] + 1 - render nothing: true + head :ok end def read_session @@ -294,12 +293,12 @@ module ApplicationTests def write_raw_session # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - render nothing: true + head :ok end def write_session session[:foo] = session[:foo] + 1 - render nothing: true + head :ok end def read_session diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb index 0a793f8f60..1a46cd3568 100644 --- a/railties/test/application/middleware/static_test.rb +++ b/railties/test/application/middleware/static_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'isolation/abstract_unit' require 'rack/test' @@ -27,5 +26,26 @@ module ApplicationTests assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set" end + + test "static_index defaults to 'index'" do + app_file "public/index.html", "/index.html" + + require "#{app_path}/config/environment" + + get '/' + + assert_equal "/index.html\n", last_response.body + end + + test "static_index configurable" do + app_file "public/other-index.html", "/other-index.html" + add_to_config "config.static_index = 'other-index'" + + require "#{app_path}/config/environment" + + get '/' + + assert_equal "/other-index.html\n", last_response.body + end end end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index c64fe082f3..ce92ebbf66 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -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", + "Rack::Lock", + "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! @@ -125,6 +152,22 @@ module ApplicationTests assert !middleware.include?("ActionDispatch::Static") end + test "can delete a middleware from the stack even if insert_before is added after delete" do + add_to_config "config.middleware.delete Rack::Runtime" + add_to_config "config.middleware.insert_before(Rack::Runtime, Rack::Config)" + boot! + assert middleware.include?("Rack::Config") + assert_not middleware.include?("Rack::Runtime") + end + + test "can delete a middleware from the stack even if insert_after is added after delete" do + add_to_config "config.middleware.delete Rack::Runtime" + add_to_config "config.middleware.insert_after(Rack::Runtime, Rack::Config)" + boot! + assert middleware.include?("Rack::Config") + assert_not middleware.include?("Rack::Runtime") + end + test "includes exceptions middlewares even if action_dispatch.show_exceptions is disabled" do add_to_config "config.action_dispatch.show_exceptions = false" boot! 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/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index c414732f92..5cc9790b28 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -181,6 +181,34 @@ module ApplicationTests end end + test "db:schema:load with inflections" do + Dir.chdir(app_path) do + app_file 'config/initializers/inflection.rb', <<-RUBY + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'goose', 'geese' + end + RUBY + app_file 'config/initializers/primary_key_table_name.rb', <<-RUBY + ActiveRecord::Base.primary_key_prefix_type = :table_name + RUBY + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table("goose".pluralize) do |t| + t.string :name + end + end + RUBY + + `bin/rake db:schema:load` + + tables = `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip + assert_match(/"geese"/, tables) + + columns = `bin/rails runner 'p ActiveRecord::Base.connection.columns("geese").map(&:name)'`.strip + assert_equal columns, '["gooseid", "name"]' + end + end + def db_test_load_structure Dir.chdir(app_path) do `rails generate model book title:string; diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb new file mode 100644 index 0000000000..ec57af79f6 --- /dev/null +++ b/railties/test/application/rake/framework_test.rb @@ -0,0 +1,48 @@ +require "isolation/abstract_unit" +require "active_support/core_ext/string/strip" + +module ApplicationTests + module RakeTests + class FrameworkTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + def load_tasks + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + end + + test 'requiring the rake task should not define method .app_generator on Object' do + require "#{app_path}/config/environment" + + load_tasks + + assert_raise NameError do + Object.method(:app_generator) + end + end + + test 'requiring the rake task should not define method .invoke_from_app_generator on Object' do + require "#{app_path}/config/environment" + + load_tasks + + assert_raise NameError do + Object.method(:invoke_from_app_generator) + end + end + end + end +end diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb new file mode 100644 index 0000000000..4cae199e6b --- /dev/null +++ b/railties/test/application/rake/restart_test.rb @@ -0,0 +1,39 @@ +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeRestartTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + 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` + curr_mtime = File.mtime("tmp/restart.txt") + 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 e8c8de9f73..4c1913f0cc 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -99,7 +99,7 @@ module ApplicationTests end def test_code_statistics_sanity - assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", + assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0", Dir.chdir(app_path){ `rake stats` } end @@ -190,17 +190,39 @@ module ApplicationTests bundle exec rake db:migrate test` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end - def test_scaffold_with_references_columns_tests_pass_by_default + 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 + `rails generate scaffold user username:string password:string; + bundle exec 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` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end @@ -227,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 DB_STRUCTURE=db/my_structure.sql` + `bundle exec rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql` end assert File.exist?(File.join(app_path, 'db', 'my_structure.sql')) end @@ -282,5 +304,12 @@ module ApplicationTests 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) { `bundle exec rake tmp:clear` } + assert_predicate $?, :success? + assert_empty errormsg + end end end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 032b11a95f..bbaab42a7f 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -1,13 +1,13 @@ 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 - ENV['RAILS_ENV'] = nil create_schema end @@ -15,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' @@ -47,16 +33,15 @@ module ApplicationTests def; end RUBY - error_stream = Tempfile.new('error') - redirect_stderr(error_stream) { run_test_command('test/models/error_test.rb') } - assert_match "syntax error", error_stream.read + error = capture(:stderr) { run_test_command('test/models/error_test.rb') } + assert_match "syntax error", error end def test_run_models create_test_file :models, 'foo' create_test_file :models, 'bar' create_test_file :controllers, 'foobar_controller' - run_test_models_command.tap do |output| + run_test_command("test/models").tap do |output| assert_match "FooTest", output assert_match "BarTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -67,7 +52,7 @@ module ApplicationTests create_test_file :helpers, 'foo_helper' create_test_file :helpers, 'bar_helper' create_test_file :controllers, 'foobar_controller' - run_test_helpers_command.tap do |output| + run_test_command("test/helpers").tap do |output| assert_match "FooHelperTest", output assert_match "BarHelperTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -75,6 +60,7 @@ module ApplicationTests end def test_run_units + skip "we no longer have the concept of unit tests. Just different directories..." create_test_file :models, 'foo' create_test_file :helpers, 'bar_helper' create_test_file :unit, 'baz_unit' @@ -91,7 +77,7 @@ module ApplicationTests create_test_file :controllers, 'foo_controller' create_test_file :controllers, 'bar_controller' create_test_file :models, 'foo' - run_test_controllers_command.tap do |output| + run_test_command("test/controllers").tap do |output| assert_match "FooControllerTest", output assert_match "BarControllerTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -102,7 +88,7 @@ module ApplicationTests create_test_file :mailers, 'foo_mailer' create_test_file :mailers, 'bar_mailer' create_test_file :models, 'foo' - run_test_mailers_command.tap do |output| + run_test_command("test/mailers").tap do |output| assert_match "FooMailerTest", output assert_match "BarMailerTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -113,7 +99,7 @@ module ApplicationTests create_test_file :jobs, 'foo_job' create_test_file :jobs, 'bar_job' create_test_file :models, 'foo' - run_test_jobs_command.tap do |output| + run_test_command("test/jobs").tap do |output| assert_match "FooJobTest", output assert_match "BarJobTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -121,6 +107,7 @@ module ApplicationTests end def test_run_functionals + skip "we no longer have the concept of functional tests. Just different directories..." create_test_file :mailers, 'foo_mailer' create_test_file :controllers, 'bar_controller' create_test_file :functional, 'baz_functional' @@ -136,7 +123,7 @@ module ApplicationTests def test_run_integration create_test_file :integration, 'foo_integration' create_test_file :models, 'foo' - run_test_integration_command.tap do |output| + run_test_command("test/integration").tap do |output| assert_match "FooIntegration", output assert_match "1 runs, 1 assertions, 0 failures", output end @@ -166,7 +153,7 @@ module ApplicationTests end RUBY - run_test_command('test/unit/chu_2_koi_test.rb test_rikka').tap do |output| + run_test_command('-n test_rikka test/unit/chu_2_koi_test.rb').tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -187,7 +174,7 @@ module ApplicationTests end RUBY - run_test_command('test/unit/chu_2_koi_test.rb /rikka/').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 @@ -195,18 +182,18 @@ module ApplicationTests def test_load_fixtures_when_running_test_suites create_model_with_fixture - suites = [:models, :helpers, [:units, :unit], :controllers, :mailers, - [:functionals, :functional], :integration] + suites = [:models, :helpers, :controllers, :mailers, :integration] suites.each do |suite, directory| directory ||= suite create_fixture_test directory - assert_match "3 users", run_task(["test:#{suite}"]) + assert_match "3 users", run_test_command("test/#{suite}") Dir.chdir(app_path) { FileUtils.rm_f "test/#{directory}" } end end def test_run_with_model + skip "These feel a bit odd. Not sure we should keep supporting them." create_model_with_fixture create_fixture_test 'models', 'user' assert_match "3 users", run_task(["test models/user"]) @@ -214,6 +201,7 @@ module ApplicationTests end def test_run_different_environment_using_env_var + skip "no longer possible. Running tests in a different environment should be explicit" app_file 'test/unit/env_test.rb', <<-RUBY require 'test_helper' @@ -228,19 +216,17 @@ module ApplicationTests assert_match "development", run_test_command('test/unit/env_test.rb') end - def test_run_different_environment_using_e_tag - env = "development" - app_file 'test/unit/env_test.rb', <<-RUBY - require 'test_helper' + def test_run_in_test_environment_by_default + create_env_test - class EnvTest < ActiveSupport::TestCase - def test_env - puts Rails.env - end - end - RUBY + assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') + end - assert_match env, run_test_command("test/unit/env_test.rb RAILS_ENV=#{env}") + def test_run_different_environment + 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 @@ -248,18 +234,115 @@ module ApplicationTests assert_match "0 failures, 0 errors, 0 skips", run_test_command('') end - private - def run_task(tasks) - Dir.chdir(app_path) { `bundle exec rake #{tasks.join ' '}` } + 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 run_test_command(arguments = 'test/unit/test_test.rb') - run_task ['test', arguments] + def test_run_with_ruby_command + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test 'declarative syntax works' do + puts 'PostTest' + assert true + end + end + RUBY + + 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 - %w{ mailers models helpers units controllers functionals integration jobs }.each do |type| - define_method("run_test_#{type}_command") do - run_task ["test:#{type}"] + end + + 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 + def run_test_command(arguments = 'test/unit/test_test.rb') + Dir.chdir(app_path) { `bin/rails t #{arguments}` } end def create_model_with_fixture @@ -292,17 +375,20 @@ module ApplicationTests RUBY end - def create_schema - app_file 'db/schema.rb', '' + 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 redirect_stderr(target_stream) - previous_stderr = STDERR.dup - $stderr.reopen(target_stream) - yield - target_stream.rewind - ensure - $stderr = previous_stderr + def create_schema + app_file 'db/schema.rb', '' end def create_test_file(path = :unit, name = 'test') @@ -318,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') } diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index c7132837b1..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,7 +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{test/unit/failing_test\.rb}, output + assert_match %r{test/unit/failing_test\.rb:4}, output end test "ruby schema migrations" do @@ -300,23 +301,7 @@ Expected: ["id", "name"] end def run_test_file(name, options = {}) - ruby '-Itest', "#{app_path}/test/#{name}", options.deep_merge(env: {"RAILS_ENV" => "test"}) - end - - def ruby(*args) - options = args.extract_options! - env = options.fetch(:env, {}) - env["RUBYLIB"] = $:.join(':') - - Dir.chdir(app_path) do - `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1` - end - end - - def env_string(variables) - variables.map do |key, value| - "#{key}='#{value}'" - end.join " " + Dir.chdir(app_path) { `bin/rails test "#{app_path}/test/#{name}" 2>&1` } end end end diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index ef16ab56ed..894e18cb39 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -42,5 +42,18 @@ module ApplicationTests get "/" assert_equal "/", last_response.body end + + def test_routes_know_the_relative_root + boot_rails + require "rails" + require "action_controller/railtie" + require "action_view/railtie" + + relative_url = '/hello' + ENV["RAILS_RELATIVE_URL_ROOT"] = relative_url + app = Class.new(Rails::Application) + assert_equal relative_url, app.routes.relative_url_root + ENV["RAILS_RELATIVE_URL_ROOT"] = nil + end end end diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index b3eabf5024..46445a001a 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -6,6 +6,43 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase @code_statistics_calculator = CodeStatisticsCalculator.new end + test 'calculate statistics using #add_by_file_path' do + code = <<-RUBY + def foo + puts 'foo' + # bar + end + RUBY + + temp_file 'stats.rb', code do |path| + @code_statistics_calculator.add_by_file_path path + + assert_equal 4, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 1, @code_statistics_calculator.methods + end + end + + test 'count number of methods in MiniTest file' do + code = <<-RUBY + class FooTest < ActionController::TestCase + test 'expectation' do + assert true + end + + def test_expectation + assert true + end + end + RUBY + + temp_file 'foo_test.rb', code do |path| + @code_statistics_calculator.add_by_file_path path + assert_equal 2, @code_statistics_calculator.methods + end + end + test 'add statistics to another using #add' do code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) @code_statistics_calculator.add(code_statistics_calculator_1) @@ -45,30 +82,6 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 6, @code_statistics_calculator.methods end - test 'calculate statistics using #add_by_file_path' do - tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) - FileUtils.mkdir_p(tmp_path) - - code = <<-'CODE' - def foo - puts 'foo' - # bar - end - CODE - - file_path = "#{tmp_path}/stats.rb" - File.open(file_path, 'w') { |f| f.write(code) } - - @code_statistics_calculator.add_by_file_path(file_path) - - assert_equal 4, @code_statistics_calculator.lines - assert_equal 3, @code_statistics_calculator.code_lines - assert_equal 0, @code_statistics_calculator.classes - assert_equal 1, @code_statistics_calculator.methods - - FileUtils.rm_rf(tmp_path) - end - test 'calculate number of Ruby methods' do code = <<-'CODE' def foo @@ -285,4 +298,17 @@ class Animal assert_equal 0, @code_statistics_calculator.classes assert_equal 0, @code_statistics_calculator.methods end + + private + def temp_file(name, content) + dir = File.expand_path '../fixtures/tmp', __FILE__ + path = "#{dir}/#{name}" + + FileUtils.mkdir_p dir + File.write path, content + + yield path + ensure + FileUtils.rm_rf path + end end diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb new file mode 100644 index 0000000000..1b1ff80bc1 --- /dev/null +++ b/railties/test/code_statistics_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' +require 'rails/code_statistics' + +class CodeStatisticsTest < ActiveSupport::TestCase + def setup + @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + @dir_js = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp', 'lib.js')) + FileUtils.mkdir_p(@dir_js) + end + + def teardown + FileUtils.rm_rf(@tmp_path) + end + + test 'ignores directories that happen to have source files extensions' do + assert_nothing_raised do + @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path]) + end + end +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index a3cd1eb0ed..7950ed6aa7 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -99,19 +99,15 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_rails_env_is_development_when_argument_is_dev - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['development', 'test']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['development', 'test']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('development', options[:environment]) end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['dev']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['dev']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('dev', options[:environment]) end end @@ -158,12 +154,6 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase assert_equal 'q1w2e3', ENV['PGPASSWORD'] end - def test_sqlite - start(adapter: 'sqlite', database: 'db') - assert !aborted - assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args - end - def test_sqlite3 start(adapter: 'sqlite3', database: 'db.sqlite3') assert !aborted diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index c4b6441397..4a4317c4f4 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 @@ -140,13 +142,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 +177,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 @@ -219,17 +240,45 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'config/routes.rb', /#{Regexp.escape(route_command)}/ end + def test_route_should_add_data_with_an_new_line + run_generator + action :route, "root 'welcome#index'" + route_path = File.expand_path("config/routes.rb", destination_root) + content = File.read(route_path) + + # Remove all of the comments and blank lines from the routes file + content.gsub!(/^ \#.*\n/, '') + content.gsub!(/^\n/, '') + + File.open(route_path, "wb") { |file| file.write(content) } + assert_file "config/routes.rb", /\.routes\.draw do\n root 'welcome#index'\nend\n\z/ + + action :route, "resources :product_lines" + + routes = <<-F +Rails.application.routes.draw do + resources :product_lines + root 'welcome#index' +end +F + assert_file "config/routes.rb", routes + end + 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.rdoc") + 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.rdoc") + 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..9978ad0da1 --- /dev/null +++ b/railties/test/generators/api_app_generator_test.rb @@ -0,0 +1,96 @@ +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_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 40fd7b77f1..af1c05cab1 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -5,7 +5,7 @@ require 'mocha/setup' # FIXME: stop using mocha DEFAULT_APP_FILES = %w( .gitignore - README.rdoc + README.md Gemfile Rakefile config.ru @@ -18,6 +18,7 @@ DEFAULT_APP_FILES = %w( app/mailers app/models app/models/concerns + app/jobs app/views/layouts bin/bundle bin/rails @@ -33,6 +34,7 @@ DEFAULT_APP_FILES = %w( log test/test_helper.rb test/fixtures + test/fixtures/files test/controllers test/models test/helpers @@ -42,6 +44,7 @@ DEFAULT_APP_FILES = %w( vendor/assets vendor/assets/stylesheets vendor/assets/javascripts + tmp tmp/cache tmp/cache/assets ) @@ -66,6 +69,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("app/assets/javascripts/application.js") end + def test_application_job_file_present + run_generator + assert_file("app/jobs/application_job.rb") + end + def test_invalid_application_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content @@ -208,6 +216,38 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) end + def test_rails_update_does_not_create_active_record_belongs_to_required_by_default + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") + + Rails.application.config.root = app_root + Rails.application.class.stubs(:name).returns("Myapp") + Rails.application.stubs(:is_a?).returns(Rails::Application) + + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end + + def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") + + Rails.application.config.root = app_root + Rails.application.class.stubs(:name).returns("Myapp") + Rails.application.stubs(:is_a?).returns(Rails::Application) + + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end + def test_application_names_are_not_singularized run_generator [File.join(destination_root, "hats")] assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/ @@ -308,6 +348,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] assert_no_file "config/database.yml" + assert_no_file "config/initializers/active_record_belongs_to_required_by_default.rb" assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ assert_file "test/test_helper.rb" do |helper_content| assert_no_match(/fixtures :all/, helper_content) @@ -409,11 +450,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_inclusion_of_doc - run_generator - assert_file 'Gemfile', /gem 'sdoc',\s+'~> 0.4.0',\s+group: :doc/ - end - def test_template_from_dir_pwd FileUtils.cd(Rails.root) assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"])) @@ -438,13 +474,13 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file 'lib/test_file.rb', 'heres test data' end - def test_test_unit_is_removed_from_frameworks_if_skip_test_unit_is_given - run_generator [destination_root, "--skip-test-unit"] + def test_tests_are_removed_from_frameworks_if_skip_test_is_given + run_generator [destination_root, "--skip-test"] assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ end - def test_no_active_record_or_test_unit_if_skips_given - run_generator [destination_root, "--skip-test-unit", "--skip-active-record"] + def test_no_active_record_or_tests_if_skips_given + run_generator [destination_root, "--skip-test", "--skip-active-record"] assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ assert_file "config/application.rb", /\s+require\s+["']active_job\/railtie["']/ @@ -525,6 +561,14 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_spring_with_dev_option + run_generator [destination_root, "--dev"] + + assert_file "Gemfile" do |content| + assert_no_match(/spring/, content) + end + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] @@ -563,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/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index a7d56dd352..1351151afb 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -96,6 +96,8 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_namespaced_routes_are_created_in_routes run_generator ["admin/dashboard", "index"] - assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/ + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n end$/, route) + end end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 94099fcd2e..62ca0ecb4b 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/core_ext/module/remove_method' +require 'active_support/testing/stream' require 'rails/generators' require 'rails/generators/test_case' @@ -23,6 +24,8 @@ require 'action_dispatch' require 'action_view' module GeneratorsTestHelper + include ActiveSupport::Testing::Stream + def self.included(base) base.class_eval do destination File.join(Rails.root, "tmp") @@ -42,21 +45,4 @@ module GeneratorsTestHelper FileUtils.cp routes, destination end - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index a9e0cea94f..7fd8f2062f 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -7,14 +7,14 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_skeleton_is_created run_generator ["refresh_counters"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) end end def test_job_queue_param run_generator ["refresh_counters", "--queue", "important"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :important/, job) end end @@ -22,7 +22,7 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_namespace run_generator ["admin/refresh_counters", "--queue", "admin"] assert_file "app/jobs/admin/refresh_counters_job.rb" do |job| - assert_match(/class Admin::RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class Admin::RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :admin/, job) end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 3d1cf87dae..f01e8cd2d9 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -7,8 +7,8 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_mailer_skeleton_is_created run_generator - assert_file "app/mailers/notifier.rb" do |mailer| - assert_match(/class Notifier < ApplicationMailer/, mailer) + assert_file "app/mailers/notifier_mailer.rb" do |mailer| + assert_match(/class NotifierMailer < ApplicationMailer/, mailer) assert_no_match(/default from: "from@example.com"/, mailer) assert_no_match(/layout :mailer_notifier/, mailer) end @@ -25,66 +25,66 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_mailer_with_i18n_helper run_generator - assert_file "app/mailers/notifier.rb" do |mailer| - assert_match(/en\.notifier\.foo\.subject/, mailer) - assert_match(/en\.notifier\.bar\.subject/, mailer) + assert_file "app/mailers/notifier_mailer.rb" do |mailer| + assert_match(/en\.notifier_mailer\.foo\.subject/, mailer) + assert_match(/en\.notifier_mailer\.bar\.subject/, mailer) end end def test_check_class_collision - Object.send :const_set, :Notifier, Class.new + Object.send :const_set, :NotifierMailer, Class.new content = capture(:stderr){ run_generator } - assert_match(/The name 'Notifier' is either already used in your application or reserved/, content) + assert_match(/The name 'NotifierMailer' is either already used in your application or reserved/, content) ensure - Object.send :remove_const, :Notifier + Object.send :remove_const, :NotifierMailer end def test_invokes_default_test_framework run_generator - assert_file "test/mailers/notifier_test.rb" do |test| - assert_match(/class NotifierTest < ActionMailer::TestCase/, test) + assert_file "test/mailers/notifier_mailer_test.rb" do |test| + assert_match(/class NotifierMailerTest < ActionMailer::TestCase/, test) assert_match(/test "foo"/, test) assert_match(/test "bar"/, test) end - assert_file "test/mailers/previews/notifier_preview.rb" do |preview| - assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview) - assert_match(/class NotifierPreview < ActionMailer::Preview/, preview) - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview) + assert_file "test/mailers/previews/notifier_mailer_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer/, preview) + assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview) assert_instance_method :foo, preview do |foo| - assert_match(/Notifier.foo/, foo) + assert_match(/NotifierMailer.foo/, foo) end - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview) assert_instance_method :bar, preview do |bar| - assert_match(/Notifier.bar/, bar) + assert_match(/NotifierMailer.bar/, bar) end end end def test_check_test_class_collision - Object.send :const_set, :NotifierTest, Class.new + Object.send :const_set, :NotifierMailerTest, Class.new content = capture(:stderr){ run_generator } - assert_match(/The name 'NotifierTest' is either already used in your application or reserved/, content) + assert_match(/The name 'NotifierMailerTest' is either already used in your application or reserved/, content) ensure - Object.send :remove_const, :NotifierTest + Object.send :remove_const, :NotifierMailerTest end def test_check_preview_class_collision - Object.send :const_set, :NotifierPreview, Class.new + Object.send :const_set, :NotifierMailerPreview, Class.new content = capture(:stderr){ run_generator } - assert_match(/The name 'NotifierPreview' is either already used in your application or reserved/, content) + assert_match(/The name 'NotifierMailerPreview' is either already used in your application or reserved/, content) ensure - Object.send :remove_const, :NotifierPreview + Object.send :remove_const, :NotifierMailerPreview end def test_invokes_default_text_template_engine run_generator - assert_file "app/views/notifier/foo.text.erb" do |view| - assert_match(%r(\sapp/views/notifier/foo\.text\.erb), view) + assert_file "app/views/notifier_mailer/foo.text.erb" do |view| + assert_match(%r(\sapp/views/notifier_mailer/foo\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end - assert_file "app/views/notifier/bar.text.erb" do |view| - assert_match(%r(\sapp/views/notifier/bar\.text\.erb), view) + assert_file "app/views/notifier_mailer/bar.text.erb" do |view| + assert_match(%r(\sapp/views/notifier_mailer/bar\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end @@ -95,13 +95,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_invokes_default_html_template_engine run_generator - assert_file "app/views/notifier/foo.html.erb" do |view| - assert_match(%r(\sapp/views/notifier/foo\.html\.erb), view) + assert_file "app/views/notifier_mailer/foo.html.erb" do |view| + assert_match(%r(\sapp/views/notifier_mailer/foo\.html\.erb), view) assert_match(/<%= @greeting %>/, view) end - assert_file "app/views/notifier/bar.html.erb" do |view| - assert_match(%r(\sapp/views/notifier/bar\.html\.erb), view) + assert_file "app/views/notifier_mailer/bar.html.erb" do |view| + assert_match(%r(\sapp/views/notifier_mailer/bar\.html\.erb), view) assert_match(/<%= @greeting %>/, view) end @@ -112,7 +112,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_invokes_default_template_engine_even_with_no_action run_generator ["notifier"] - assert_file "app/views/notifier" + assert_file "app/views/notifier_mailer" assert_file "app/views/layouts/mailer.text.erb" assert_file "app/views/layouts/mailer.html.erb" end @@ -124,23 +124,23 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_mailer_with_namedspaced_mailer run_generator ["Farm::Animal", "moos"] - assert_file "app/mailers/farm/animal.rb" do |mailer| - assert_match(/class Farm::Animal < ApplicationMailer/, mailer) - assert_match(/en\.farm\.animal\.moos\.subject/, mailer) + assert_file "app/mailers/farm/animal_mailer.rb" do |mailer| + assert_match(/class Farm::AnimalMailer < ApplicationMailer/, mailer) + assert_match(/en\.farm\.animal_mailer\.moos\.subject/, mailer) end - assert_file "test/mailers/previews/farm/animal_preview.rb" do |preview| - assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview) - assert_match(/class Farm::AnimalPreview < ActionMailer::Preview/, preview) - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview) + assert_file "test/mailers/previews/farm/animal_mailer_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal_mailer/, preview) + assert_match(/class Farm::AnimalMailerPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal_mailer\/moos/, preview) end - assert_file "app/views/farm/animal/moos.text.erb" - assert_file "app/views/farm/animal/moos.html.erb" + assert_file "app/views/farm/animal_mailer/moos.text.erb" + assert_file "app/views/farm/animal_mailer/moos.html.erb" end def test_actions_are_turned_into_methods run_generator - assert_file "app/mailers/notifier.rb" do |mailer| + assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_instance_method :foo, mailer do |foo| assert_match(/mail to: "to@example.org"/, foo) assert_match(/@greeting = "Hi"/, foo) @@ -167,4 +167,20 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_file "app/views/layouts/mailer.text.erb" assert_file "app/views/layouts/mailer.html.erb" end + + def test_mailer_suffix_is_not_duplicated + run_generator ["notifier_mailer"] + + assert_no_file "app/mailers/notifier_mailer_mailer.rb" + assert_file "app/mailers/notifier_mailer.rb" + + assert_no_file "app/views/notifier_mailer_mailer/" + assert_file "app/views/notifier_mailer/" + + assert_no_file "test/mailers/notifier_mailer_mailer_test.rb" + assert_file "test/mailers/notifier_mailer_test.rb" + + assert_no_file "test/mailers/previews/notifier_mailer_mailer_preview.rb" + assert_file "test/mailers/previews/notifier_mailer_preview.rb" + end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 413d457d54..57bc220558 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -276,6 +276,30 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_create_table_migration_with_token_option + run_generator ["create_users", "token:token", "auth_token:token"] + assert_migration "db/migrate/create_users.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :users/, change) + assert_match(/ t\.string :token/, change) + assert_match(/ t\.string :auth_token/, change) + assert_match(/add_index :users, :token, unique: true/, change) + assert_match(/add_index :users, :auth_token, unique: true/, change) + end + end + end + + def test_add_migration_with_token_option + migration = "add_token_to_users" + run_generator [migration, "auth_token:token"] + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_column :users, :auth_token, :string/, change) + assert_match(/add_index :users, :auth_token, unique: true/, change) + end + end + end + private def with_singular_table_name diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index f3b699101f..abd3ff50a4 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -319,6 +319,16 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_file "test/fixtures/accounts.yml" end + def test_fixture_without_pluralization + original_pluralize_table_name = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + run_generator + assert_generated_fixture("test/fixtures/account.yml", + {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name + end + def test_check_class_collision content = capture(:stderr){ run_generator ["object"] } assert_match(/The name 'Object' is either already used in your application or reserved/, content) @@ -438,6 +448,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_token_option_adds_has_secure_token + run_generator ["user", "token:token", "auth_token:token"] + expected_file = <<-FILE.strip_heredoc + class User < ActiveRecord::Base + has_secure_token + has_secure_token :auth_token + end + FILE + assert_file "app/models/user.rb", expected_file + end + private def assert_generated_fixture(path, parsed_contents) fixture_file = File.new File.expand_path(path, destination_root) diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 4199e00b0d..1c32fc1bfd 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,16 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' -require 'mocha/setup' # FIXME: stop using mocha - -# Mock out what we need from AR::Base. -module ActiveRecord - class Base - class << self - attr_accessor :pluralize_table_names - end - self.pluralize_table_names = true - end -end +require 'minitest/mock' class NamedBaseTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -59,11 +49,13 @@ class NamedBaseTest < Rails::Generators::TestCase end def test_named_generator_attributes_without_pluralized + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names ActiveRecord::Base.pluralize_table_names = false + g = generator ['admin/foo'] assert_name g, 'admin_foo', :table_name ensure - ActiveRecord::Base.pluralize_table_names = true + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names end def test_scaffold_plural_names @@ -88,10 +80,13 @@ class NamedBaseTest < Rails::Generators::TestCase def test_application_name g = generator ['Admin::Foo'] - Rails.stubs(:application).returns(Object.new) - assert_name g, "object", :application_name - Rails.stubs(:application).returns(nil) - assert_name g, "application", :application_name + Rails.stub(:application, Object.new) do + assert_name g, "object", :application_name + end + + Rails.stub(:application, nil) do + assert_name g, "application", :application_name + end end def test_index_helper @@ -111,11 +106,11 @@ class NamedBaseTest < Rails::Generators::TestCase def test_hide_namespace g = generator ['Hidden'] - g.class.stubs(:namespace).returns('hidden') - - assert !Rails::Generators.hidden_namespaces.include?('hidden') - g.class.hide! - assert Rails::Generators.hidden_namespaces.include?('hidden') + g.class.stub(:namespace, 'hidden') do + assert !Rails::Generators.hidden_namespaces.include?('hidden') + g.class.hide! + assert Rails::Generators.hidden_namespaces.include?('hidden') + end end def test_scaffold_plural_names_with_model_name_option diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index 6075805152..e839b67960 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -146,26 +146,26 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase def test_mailer_skeleton_is_created run_generator - assert_file "app/mailers/test_app/notifier.rb" do |mailer| + assert_file "app/mailers/test_app/notifier_mailer.rb" do |mailer| assert_match(/module TestApp/, mailer) - assert_match(/class Notifier < ApplicationMailer/, mailer) + assert_match(/class NotifierMailer < ApplicationMailer/, mailer) assert_no_match(/default from: "from@example.com"/, mailer) end end def test_mailer_with_i18n_helper run_generator - assert_file "app/mailers/test_app/notifier.rb" do |mailer| - assert_match(/en\.notifier\.foo\.subject/, mailer) - assert_match(/en\.notifier\.bar\.subject/, mailer) + assert_file "app/mailers/test_app/notifier_mailer.rb" do |mailer| + assert_match(/en\.notifier_mailer\.foo\.subject/, mailer) + assert_match(/en\.notifier_mailer\.bar\.subject/, mailer) end end def test_invokes_default_test_framework run_generator - assert_file "test/mailers/test_app/notifier_test.rb" do |test| + assert_file "test/mailers/test_app/notifier_mailer_test.rb" do |test| assert_match(/module TestApp/, test) - assert_match(/class NotifierTest < ActionMailer::TestCase/, test) + assert_match(/class NotifierMailerTest < ActionMailer::TestCase/, test) assert_match(/test "foo"/, test) assert_match(/test "bar"/, test) end @@ -173,20 +173,20 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase def test_invokes_default_template_engine run_generator - assert_file "app/views/test_app/notifier/foo.text.erb" do |view| - assert_match(%r(app/views/test_app/notifier/foo\.text\.erb), view) + assert_file "app/views/test_app/notifier_mailer/foo.text.erb" do |view| + assert_match(%r(app/views/test_app/notifier_mailer/foo\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end - assert_file "app/views/test_app/notifier/bar.text.erb" do |view| - assert_match(%r(app/views/test_app/notifier/bar\.text\.erb), view) + assert_file "app/views/test_app/notifier_mailer/bar.text.erb" do |view| + assert_match(%r(app/views/test_app/notifier_mailer/bar\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end end def test_invokes_default_template_engine_even_with_no_action run_generator ["notifier"] - assert_file "app/views/test_app/notifier" + assert_file "app/views/test_app/notifier_mailer" end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 318ea5b2cb..d6e5f4bd89 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -28,23 +28,30 @@ class PluginGeneratorTest < Rails::Generators::TestCase include SharedGeneratorTests def test_invalid_plugin_name_raises_an_error - content = capture(:stderr){ run_generator [File.join(destination_root, "things-43")] } - assert_equal "Invalid plugin name things-43. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content + content = capture(:stderr){ run_generator [File.join(destination_root, "my_plugin-31fr-extension")] } + assert_equal "Invalid plugin name my_plugin-31fr-extension. Please give a name which does not contain a namespace starting with numeric characters.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] } - assert_equal "Invalid plugin name things4.3. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content + assert_equal "Invalid plugin name things4.3. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] } 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.\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 end - def test_camelcase_plugin_name_underscores_filenames + def test_correct_file_in_lib_folder_of_hyphenated_plugin_name + run_generator [File.join(destination_root, "hyphenated-name")] + assert_no_file "hyphenated-name/lib/hyphenated-name.rb" + assert_no_file "hyphenated-name/lib/hyphenated_name.rb" + assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here...\n end\nend/ + end + + def test_correct_file_in_lib_folder_of_camelcase_plugin_name run_generator [File.join(destination_root, "CamelCasedName")] assert_no_file "CamelCasedName/lib/CamelCasedName.rb" assert_file "CamelCasedName/lib/camel_cased_name.rb", /module CamelCasedName/ @@ -112,7 +119,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_ensure_that_test_dummy_can_be_generated_from_a_template FileUtils.cd(Rails.root) - run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test-unit"]) + run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test"]) assert_file "spec/dummy" assert_no_file "test" end @@ -185,13 +192,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_generation_runs_bundle_install_with_full_and_mountable result = run_generator [destination_root, "--mountable", "--full", "--dev"] + assert_match(/run bundle install/, result) + assert $?.success?, "Command failed: #{result}" assert_file "#{destination_root}/Gemfile.lock" do |contents| assert_match(/bukkits/, contents) end - assert_match(/run bundle install/, result) - assert_match(/Using bukkits \(?0\.0\.1\)?/, result) - assert_match(/Your bundle is complete/, result) - assert_equal 1, result.scan("Your bundle is complete").size end def test_skipping_javascripts_without_mountable_option @@ -252,6 +257,40 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "lib/bukkits.rb", /require "bukkits\/engine"/ end + def test_creating_engine_with_hyphenated_name_in_full_mode + run_generator [File.join(destination_root, "hyphenated-name"), "--full"] + assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" + assert_file "hyphenated-name/app/assets/images/hyphenated/name" + assert_file "hyphenated-name/app/models" + assert_file "hyphenated-name/app/controllers" + assert_file "hyphenated-name/app/views" + assert_file "hyphenated-name/app/helpers" + assert_file "hyphenated-name/app/mailers" + assert_file "hyphenated-name/bin/rails" + assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ + assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ + assert_file "hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/hyphenated\/name\/engine/ + end + + def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode + run_generator [File.join(destination_root, "my_hyphenated-name"), "--full"] + assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" + assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" + assert_file "my_hyphenated-name/app/models" + assert_file "my_hyphenated-name/app/controllers" + assert_file "my_hyphenated-name/app/views" + assert_file "my_hyphenated-name/app/helpers" + assert_file "my_hyphenated-name/app/mailers" + assert_file "my_hyphenated-name/bin/rails" + assert_file "my_hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ + assert_file "my_hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/my_hyphenated\/name\/engine/ + end + def test_being_quiet_while_creating_dummy_application assert_no_match(/create\s+config\/application.rb/, run_generator) end @@ -277,6 +316,63 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_create_mountable_application_with_mountable_option_and_hypenated_name + run_generator [File.join(destination_root, "hyphenated-name"), "--mountable"] + assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_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/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ + assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ + assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/ + assert_file "hyphenated-name/app/helpers/hyphenated/name/application_helper.rb", /module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ + assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents| + assert_match "<title>Hyphenated name</title>", contents + assert_match(/stylesheet_link_tag\s+['"]hyphenated\/name\/application['"]/, contents) + assert_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents) + end + end + + def test_create_mountable_application_with_mountable_option_and_hypenated_and_underscored_name + run_generator [File.join(destination_root, "my_hyphenated-name"), "--mountable"] + assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_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/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ + assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ + assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/ + assert_file "my_hyphenated-name/app/helpers/my_hyphenated/name/application_helper.rb", /module MyHyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ + assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents| + assert_match "<title>My hyphenated name</title>", contents + assert_match(/stylesheet_link_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) + assert_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) + end + end + + def test_create_mountable_application_with_mountable_option_and_multiple_hypenates_in_name + run_generator [File.join(destination_root, "deep-hyphenated-name"), "--mountable"] + assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" + assert_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/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ + assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ + assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/helpers/deep/hyphenated/name/application_helper.rb", /module Deep\n module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents| + assert_match "<title>Deep hyphenated name</title>", contents + assert_match(/stylesheet_link_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) + assert_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) + end + end + def test_creating_gemspec run_generator assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ @@ -321,7 +417,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase end def test_creating_dummy_without_tests_but_with_dummy_path - run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"] + run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test"] assert_file "spec/dummy" assert_file "spec/dummy/config/application.rb" assert_no_file "test" @@ -333,14 +429,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path FileUtils.cd(Rails.root) - run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"]) + run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test"]) assert_file ".gitignore" do |contents| assert_match(/spec\/dummy/, contents) end end - def test_skipping_test_unit - run_generator [destination_root, "--skip-test-unit"] + def test_skipping_test_files + run_generator [destination_root, "--skip-test"] assert_no_file "test" assert_file "bukkits.gemspec" do |contents| assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents) diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index ca972a3bdd..5dae36b65e 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -106,8 +106,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase 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, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \}/, content) - assert_match(/patch :update, id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \}/, 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) end end @@ -117,8 +117,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase 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, user: \{ \}/, content) - assert_match(/patch :update, id: @user, user: \{ \}/, content) + assert_match(/post :create, params: \{ user: \{ \} \}/, content) + assert_match(/patch :update, params: \{ id: @user, user: \{ \} \}/, content) end end @@ -174,4 +174,62 @@ 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/, `bundle exec rake 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 3b545328b5..3401b96d7d 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -58,8 +58,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "test/controllers/product_lines_controller_test.rb" do |test| assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test) - assert_match(/post :create, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \}/, test) - assert_match(/patch :update, id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \}/, 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) end # Views @@ -87,14 +87,84 @@ 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"] assert_file "test/controllers/product_lines_controller_test.rb" do |content| assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, product_line: \{ \}/, content) - assert_match(/patch :update, id: @product_line, product_line: \{ \}/, content) + assert_match(/post :create, params: \{ product_line: \{ \} \}/, content) + assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ \} \}/, content) end end @@ -245,6 +315,29 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/ end + def test_scaffold_generator_on_revoke_does_not_mutilate_routes + run_generator + + route_path = File.expand_path("config/routes.rb", destination_root) + content = File.read(route_path) + + # Remove all of the comments and blank lines from the routes file + content.gsub!(/^ \#.*\n/, '') + content.gsub!(/^\n/, '') + + File.open(route_path, "wb") { |file| file.write(content) } + assert_file "config/routes.rb", /\.routes\.draw do\n resources :product_lines\nend\n\z/ + + run_generator ["product_line"], :behavior => :revoke + + assert_file "config/routes.rb", /\.routes\.draw do\nend\n\z/ + end + + def test_scaffold_generator_ignores_commented_routes + run_generator ["product"] + assert_file "config/routes.rb", /\.routes\.draw do\n resources :products\n/ + end + def test_scaffold_generator_no_assets_with_switch_no_assets run_generator [ "posts", "--no-assets" ] assert_no_file "app/assets/stylesheets/scaffold.css" @@ -259,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| @@ -370,4 +477,18 @@ 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/, `bundle exec rake 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 b998fef42e..f983b45d2b 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -47,8 +47,8 @@ module SharedGeneratorTests assert_match(/Invalid value for \-\-database option/, content) end - def test_test_unit_is_skipped_if_required - run_generator [destination_root, "--skip-test-unit"] + def test_test_files_are_skipped_if_required + run_generator [destination_root, "--skip-test"] assert_no_file "test" end @@ -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.\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!" } @@ -138,7 +129,11 @@ module SharedGeneratorTests def test_skip_keeps run_generator [destination_root, '--skip-keeps', '--full'] - assert_file('.gitignore') + + assert_file '.gitignore' do |content| + assert_no_match(/\.keep/, content) + end + assert_no_file('app/mailers/.keep') end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 127e059bf1..31a575749a 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,7 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' require 'rails/generators/test_unit/model/model_generator' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -9,16 +9,20 @@ class GeneratorsTest < Rails::Generators::TestCase def setup @path = File.expand_path("lib", Rails.root) $LOAD_PATH.unshift(@path) + @mock_generator = MiniTest::Mock.new end def teardown $LOAD_PATH.delete(@path) + @mock_generator.verify end def test_simple_invoke assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke("test_unit:model", ["Account"]) + @mock_generator.expect(:call, nil, [["Account"],{}]) + TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + Rails::Generators.invoke("test_unit:model", ["Account"]) + end end def test_invoke_when_generator_is_not_found @@ -34,6 +38,12 @@ class GeneratorsTest < Rails::Generators::TestCase assert_match "Maybe you meant 'migration'", output end + def test_generator_multiple_suggestions + name = :tas + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Maybe you meant 'task', 'job' or", output + end + def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments output = capture(:stdout){ Rails::Generators.invoke :model, [] } assert_match(/Description:/, output) @@ -41,19 +51,25 @@ class GeneratorsTest < Rails::Generators::TestCase def test_should_give_higher_preference_to_rails_generators assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } - assert warnings.empty? + @mock_generator.expect(:call, nil, [["Account"],{}]) + Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do + warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } + assert warnings.empty? + end end def test_invoke_with_default_values - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke :model, ["Account"] + @mock_generator.expect(:call, nil, [["Account"],{}]) + Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do + Rails::Generators.invoke :model, ["Account"] + end end def test_invoke_with_config_values - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], behavior: :skip) - Rails::Generators.invoke :model, ["Account"], behavior: :skip + @mock_generator.expect(:call, nil, [["Account"],{behavior: :skip}]) + Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do + Rails::Generators.invoke :model, ["Account"], behavior: :skip + end end def test_find_by_namespace @@ -97,11 +113,13 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_invoke_with_nested_namespaces - model_generator = mock('ModelGenerator') do - expects(:start).with(["Account"], {}) + model_generator = Minitest::Mock.new + model_generator.expect(:start, nil, [["Account"], {}]) + @mock_generator.expect(:call, model_generator, ['namespace', 'my:awesome']) + Rails::Generators.stub(:find_by_namespace, @mock_generator) do + Rails::Generators.invoke 'my:awesome:namespace', ["Account"] end - Rails::Generators.expects(:find_by_namespace).with('namespace', 'my:awesome').returns(model_generator) - Rails::Generators.invoke 'my:awesome:namespace', ["Account"] + model_generator.verify end def test_rails_generators_help_with_builtin_information @@ -167,8 +185,10 @@ class GeneratorsTest < Rails::Generators::TestCase def test_fallbacks_for_generators_on_invoke Rails::Generators.fallbacks[:shoulda] = :test_unit - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke "shoulda:model", ["Account"] + @mock_generator.expect(:call, nil, [["Account"],{}]) + TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + Rails::Generators.invoke "shoulda:model", ["Account"] + end ensure Rails::Generators.fallbacks.delete(:shoulda) end @@ -176,8 +196,10 @@ class GeneratorsTest < Rails::Generators::TestCase def test_nested_fallbacks_for_generators Rails::Generators.fallbacks[:shoulda] = :test_unit Rails::Generators.fallbacks[:super_shoulda] = :shoulda - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke "super_shoulda:model", ["Account"] + @mock_generator.expect(:call, nil, [["Account"],{}]) + TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + Rails::Generators.invoke "super_shoulda:model", ["Account"] + end ensure Rails::Generators.fallbacks.delete(:shoulda) Rails::Generators.fallbacks.delete(:super_shoulda) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 39e8a5f756..65d8a55421 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -11,6 +11,7 @@ require 'fileutils' require 'bundler/setup' unless defined?(Bundler) require 'active_support' require 'active_support/testing/autorun' +require 'active_support/testing/stream' require 'active_support/test_case' RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") @@ -69,7 +70,8 @@ module TestHelpers def assert_welcome(resp) assert_equal 200, resp[0] - assert resp[1]["Content-Type"] = "text/html" + assert_match 'text/html', resp[1]["Content-Type"] + assert_match 'charset=utf-8', resp[1]["Content-Type"] assert extract_body(resp).match(/Welcome aboard/) end @@ -268,19 +270,13 @@ module TestHelpers File.open(file, "w+") { |f| f.puts contents } end - def app_file(path, contents) + def app_file(path, contents, mode = 'w') FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", 'w') do |f| + File.open("#{app_path}/#{path}", mode) do |f| f.puts contents end end - def gsub_app_file(path, regexp, *args, &block) - path = "#{app_path}/#{path}" - content = File.read(path).gsub(regexp, *args, &block) - File.open(path, 'wb') { |f| f.write(content) } - end - def remove_file(path) FileUtils.rm_rf "#{app_path}/#{path}" end @@ -309,45 +305,10 @@ class ActiveSupport::TestCase include TestHelpers::Paths include TestHelpers::Rack include TestHelpers::Generation + include ActiveSupport::Testing::Stream self.test_order = :sorted - private - - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end - - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end # Create a scope and build a fixture rails app @@ -361,7 +322,7 @@ Module.new do environment = File.expand_path('../../../../load_paths', __FILE__) require_environment = "-r #{environment}" - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile --no-rc` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc` File.open("#{app_template_path}/config/boot.rb", 'w') do |f| f.puts "require '#{environment}'" f.puts "require 'rails/all'" diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index 13bf29d3c3..27e64b97b7 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -1,22 +1,17 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_support/core_ext/object/with_options' require 'active_support/core_ext/object/json' -require 'rails' -require 'rails/application' - -ROUTING = ActionDispatch::Routing class PathGenerationTest < ActiveSupport::TestCase attr_reader :app - class TestSet < ROUTING::RouteSet + class TestSet < ActionDispatch::Routing::RouteSet def initialize(block) @block = block super() end - class Dispatcher < ROUTING::RouteSet::Dispatcher + class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher def initialize(defaults, set, block) super(defaults) @block = block diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 1aeb9ec339..12630e4d01 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -1,10 +1,9 @@ require 'abstract_unit' require 'rails/paths' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' class PathsTest < ActiveSupport::TestCase def setup - File.stubs(:exist?).returns(true) @root = Rails::Paths::Root.new("/foo/bar") end @@ -93,10 +92,12 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path that should be autoloaded only once" do - @root.add "app", with: "/app" - @root["app"].autoload_once! - assert @root["app"].autoload_once? - assert @root.autoload_once.include?(@root["app"].expanded.first) + File.stub(:exist?, true) do + @root.add "app", with: "/app" + @root["app"].autoload_once! + assert @root["app"].autoload_once? + assert @root.autoload_once.include?(@root["app"].expanded.first) + end end test "it is possible to remove a path that should be autoloaded only once" do @@ -110,37 +111,47 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path without assignment and specify it should be loaded only once" do - @root.add "app", with: "/app", autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", autoload_once: true + assert @root["app"].autoload_once? + assert @root.autoload_once.include?("/app") + end end test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do - @root.add "app", with: ["/app", "/app2"], autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") - assert @root.autoload_once.include?("/app2") + File.stub(:exist?, true) do + @root.add "app", with: ["/app", "/app2"], autoload_once: true + assert @root["app"].autoload_once? + assert @root.autoload_once.include?("/app") + assert @root.autoload_once.include?("/app2") + end end test "making a path autoload_once more than once only includes it once in @root.load_once" do - @root["app"] = "/app" - @root["app"].autoload_once! - @root["app"].autoload_once! - assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].autoload_once! + @root["app"].autoload_once! + assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size + end end test "paths added to a load_once path should be added to the autoload_once collection" do - @root["app"] = "/app" - @root["app"].autoload_once! - @root["app"] << "/app2" - assert_equal 2, @root.autoload_once.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].autoload_once! + @root["app"] << "/app2" + assert_equal 2, @root.autoload_once.size + end end test "it is possible to mark a path as eager loaded" do - @root["app"] = "/app" - @root["app"].eager_load! - assert @root["app"].eager_load? - assert @root.eager_load.include?(@root["app"].to_a.first) + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + assert @root["app"].eager_load? + assert @root.eager_load.include?(@root["app"].to_a.first) + end end test "it is possible to skip a path from eager loading" do @@ -154,38 +165,48 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path without assignment and mark it as eager" do - @root.add "app", with: "/app", eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", eager_load: true + assert @root["app"].eager_load? + assert @root.eager_load.include?("/app") + end end test "it is possible to add multiple paths without assignment and mark them as eager" do - @root.add "app", with: ["/app", "/app2"], eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") - assert @root.eager_load.include?("/app2") + File.stub(:exist?, true) do + @root.add "app", with: ["/app", "/app2"], eager_load: true + assert @root["app"].eager_load? + assert @root.eager_load.include?("/app") + assert @root.eager_load.include?("/app2") + end end test "it is possible to create a path without assignment and mark it both as eager and load once" do - @root.add "app", with: "/app", eager_load: true, autoload_once: true - assert @root["app"].eager_load? - assert @root["app"].autoload_once? - assert @root.eager_load.include?("/app") - assert @root.autoload_once.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", eager_load: true, autoload_once: true + assert @root["app"].eager_load? + assert @root["app"].autoload_once? + assert @root.eager_load.include?("/app") + assert @root.autoload_once.include?("/app") + end end test "making a path eager more than once only includes it once in @root.eager_paths" do - @root["app"] = "/app" - @root["app"].eager_load! - @root["app"].eager_load! - assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + @root["app"].eager_load! + assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size + end end test "paths added to an eager_load path should be added to the eager_load collection" do - @root["app"] = "/app" - @root["app"].eager_load! - @root["app"] << "/app2" - assert_equal 2, @root.eager_load.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + @root["app"] << "/app2" + assert_equal 2, @root.eager_load.size + end end test "it should be possible to add a path's default glob" do @@ -207,28 +228,36 @@ class PathsTest < ActiveSupport::TestCase end test "a path can be added to the load path" do - @root["app"] = "app" - @root["app"].load_path! - @root["app/models"] = "app/models" - assert_equal ["/foo/bar/app"], @root.load_paths + File.stub(:exist?, true) do + @root["app"] = "app" + @root["app"].load_path! + @root["app/models"] = "app/models" + assert_equal ["/foo/bar/app"], @root.load_paths + end end test "a path can be added to the load path on creation" do - @root.add "app", with: "/app", load_path: true - assert @root["app"].load_path? - assert_equal ["/app"], @root.load_paths + File.stub(:exist?, true) do + @root.add "app", with: "/app", load_path: true + assert @root["app"].load_path? + assert_equal ["/app"], @root.load_paths + end end test "a path can be marked as autoload path" do - @root["app"] = "app" - @root["app"].autoload! - @root["app/models"] = "app/models" - assert_equal ["/foo/bar/app"], @root.autoload_paths + File.stub(:exist?, true) do + @root["app"] = "app" + @root["app"].autoload! + @root["app/models"] = "app/models" + assert_equal ["/foo/bar/app"], @root.autoload_paths + end end test "a path can be marked as autoload on creation" do - @root.add "app", with: "/app", autoload: true - assert @root["app"].autoload? - assert_equal ["/app"], @root.autoload_paths + File.stub(:exist?, true) do + @root.add "app", with: "/app", autoload: true + assert @root["app"].autoload? + assert_equal ["/app"], @root.autoload_paths + end end end diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index d336a2e6c0..c51503c2b7 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -16,7 +16,7 @@ class InfoControllerTest < ActionController::TestCase end @routes = Rails.application.routes - Rails::InfoController.send(:include, @routes.url_helpers) + Rails::InfoController.include(@routes.url_helpers) @request.env["REMOTE_ADDR"] = "127.0.0.1" end @@ -53,4 +53,29 @@ class InfoControllerTest < ActionController::TestCase assert_response :success end + test "info controller returns exact matches" do + exact_count = -> { JSON(response.body)['exact'].size } + + get :routes, params: { path: 'rails/info/route' } + assert exact_count.call == 0, 'should not match incomplete routes' + + get :routes, params: { path: 'rails/info/routes' } + assert exact_count.call == 1, 'should match complete routes' + + get :routes, params: { path: 'rails/info/routes.html' } + assert exact_count.call == 1, 'should match complete routes with optional parts' + end + + test "info controller returns fuzzy matches" do + fuzzy_count = -> { JSON(response.body)['fuzzy'].size } + + get :routes, params: { path: 'rails/info' } + assert fuzzy_count.call == 2, 'should match incomplete routes' + + get :routes, params: { path: 'rails/info/routes' } + assert fuzzy_count.call == 1, 'should match complete routes' + + get :routes, params: { path: 'rails/info/routes.html' } + assert fuzzy_count.call == 0, 'should match optional parts of route literally' + end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 6185742cc1..79bd7a8241 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1155,10 +1155,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js\n;", last_response.body.strip + assert_equal "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js\n;", last_response.body.strip + assert_equal "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 7348d70c56..423ece277e 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -30,7 +30,7 @@ module RailtiesTests if File.exist?("#{environment}.rb") require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{cmd}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end def build_engine(is_mountable=false) diff --git a/railties/test/test_info_test.rb b/railties/test/test_info_test.rb deleted file mode 100644 index b9c3a9c0c7..0000000000 --- a/railties/test/test_info_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'abstract_unit' -require 'rails/test_unit/sub_test_task' - -module Rails - class TestInfoTest < ActiveSupport::TestCase - def test_test_files - info = new_test_info ['test'] - assert_predicate info.files, :empty? - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_file - info = new_test_info ['test', __FILE__] - assert_equal [__FILE__], info.files - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_opts - info = new_test_info ['test', __FILE__, '/foo/'] - assert_equal [__FILE__], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_shorthand - info = new_test_info ['test', 'models/foo', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_path - info = new_test_info ['test', 'app/models/foo.rb', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - private - def new_test_info(tasks) - Class.new(TestTask::TestInfo) { - def task_defined?(task) - task == "test" - end - }.new tasks - end - end -end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb new file mode 100644 index 0000000000..77883612f5 --- /dev/null +++ b/railties/test/test_unit/reporter_test.rb @@ -0,0 +1,74 @@ +require 'abstract_unit' +require 'rails/test_unit/reporter' + +class TestUnitReporterTest < ActiveSupport::TestCase + class ExampleTest < Minitest::Test + def woot; end + end + + setup do + @output = StringIO.new + @reporter = Rails::TestUnitReporter.new @output + end + + test "prints rerun snippet to run a single failed test" do + @reporter.record(failed_test) + @reporter.report + + assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_rerun_snippet_count 1 + end + + test "prints rerun snippet for every failed test" do + @reporter.record(failed_test) + @reporter.record(failed_test) + @reporter.record(failed_test) + @reporter.report + + assert_rerun_snippet_count 3 + end + + test "does not print snippet for successful and skipped tests" do + @reporter.record(passing_test) + @reporter.record(skipped_test) + @reporter.report + assert_rerun_snippet_count 0 + end + + test "prints rerun snippet for skipped tests if run in verbose mode" do + verbose = Rails::TestUnitReporter.new @output, verbose: true + verbose.record(skipped_test) + verbose.report + + assert_rerun_snippet_count 1 + end + + private + def assert_rerun_snippet_count(snippet_count) + assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size + end + + def failed_test + ft = ExampleTest.new(:woot) + ft.failures << begin + raise Minitest::Assertion, "boo" + rescue Minitest::Assertion => e + e + end + ft + end + + def passing_test + ExampleTest.new(:woot) + end + + def skipped_test + st = ExampleTest.new(:woot) + st.failures << begin + raise Minitest::Skip + rescue Minitest::Assertion => e + e + end + st + end +end diff --git a/tools/line_statistics b/tools/line_statistics index bfa921b095..d0b3557d7d 100644 --- a/tools/line_statistics +++ b/tools/line_statistics @@ -1,4 +1,4 @@ -# Class used to calculates LOC for a provided file list. +# Class used to calculate LOC for a provided file list. # # Example: # files = FileList["lib/active_record/**/*.rb"] 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..50cce2081a --- /dev/null +++ b/tools/test.rb @@ -0,0 +1,10 @@ +$: << 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 |