diff options
1212 files changed, 25402 insertions, 13809 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 985956363a..3130ee1b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,38 @@ language: ruby +sudo: false script: 'ci/travis.rb' before_install: - - travis_retry gem install bundler - - "rvm current | grep 'jruby' && export AR_JDBC=true || echo" -rvm: - - 1.9.3 - - 2.0.0 - - 2.1 - - ruby-head - - rbx-2 - - jruby + - gem install bundler + - "rm ${BUNDLE_GEMFILE}.lock" +before_script: + - bundle update +cache: bundler env: global: - - JRUBY_OPTS='-J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-Djruby.compile.mode=OFF -J-Djruby.compile.invokedynamic=false -J-Xmx1024M' + - JRUBY_OPTS='-J-Xmx1024M' matrix: - "GEM=railties" - "GEM=ap" - - "GEM=am,amo,as,av,aj" + - "GEM=aj" + - "GEM=am,amo,as,av" - "GEM=ar:mysql" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - "GEM=aj:integration" +rvm: + - 2.2 + - ruby-head + - rbx-2 + - jruby-head matrix: allow_failures: - - rvm: 1.9.3 - env: "GEM=ar:mysql" - - rvm: 2.0.0 - env: "GEM=ar:mysql" - - rvm: rbx-2 - - rvm: jruby + - env: "GEM=ar:mysql" - rvm: ruby-head + - rvm: rbx-2 + - rvm: jruby-head + - env: "GEM=aj" + - env: "GEM=aj:integration" fast_finish: true notifications: email: false @@ -44,11 +46,10 @@ notifications: on_failure: always rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" -bundler_args: --path vendor/bundle --without test +bundler_args: --without test --jobs 3 --retry 3 services: - memcached - redis - rabbitmq addons: postgresql: "9.3" - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ba2e53ef2..421e7088cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ 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). @@ -2,21 +2,19 @@ source 'https://rubygems.org' gemspec +# We need a newish Rake since Active Job sets its test tasks' descriptions. +gem 'rake', '>= 10.3' + # This needs to be with require false as it is # loaded after loading the test library to # ensure correct loading order gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' -gem 'jquery-rails', '~> 4.0.0.beta2' -gem 'coffee-rails', '~> 4.0.0' -# TODO: Release this gem -gem 'rails-html-sanitizer', github: 'rails/rails-html-sanitizer' -# TODO: Release this gem -gem 'rails-deprecated_sanitizer', github: 'rails/rails-deprecated_sanitizer' -# TODO: Release this gem -gem 'rails-dom-testing', github: 'rails/rails-dom-testing' -gem 'turbolinks', '~> 2.2.3' +gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master' +gem 'coffee-rails', '~> 4.1.0' +gem 'turbolinks' +gem 'arel', github: 'rails/arel', 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) @@ -29,9 +27,9 @@ gem 'uglifier', '>= 1.3.0', require: false group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 3.1.2', platforms: :ruby + gem 'redcarpet', '~> 3.2.2', platforms: :ruby gem 'w3c_validators' - gem 'kindlerb' + gem 'kindlerb', '0.1.1' end # AS @@ -44,13 +42,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 'queue_classic', require: false, platforms: :ruby gem 'sneakers', '0.1.1.pre', 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 @@ -66,8 +64,12 @@ group :test do gem 'ruby-prof', '~> 0.11.2' end - # platforms :mri_19, :mri_20 do - # gem 'debugger' + platforms :mri_21, :mri_22 do + gem 'stackprof' + end + + # platforms :mri do + # gem 'byebug' # end gem 'benchmark-ips' @@ -83,9 +85,9 @@ platforms :ruby do gem 'sqlite3', '~> 1.3.6' group :db do - gem 'pg', '>= 0.11.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..d2ed998160 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,281 @@ +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/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) + +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.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.1) + 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.1) + 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) + 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) + beaneater (0.3.3) + benchmark-ips (2.1.1) + builder (3.2.2) + bunny (1.1.9) + amq-protocol (>= 1.9.2) + 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) + connection_pool (2.1.1) + dalli (2.7.2) + dante (0.1.5) + delayed_job (4.0.6) + activesupport (>= 3.0, < 5.0) + erubis (2.7.0) + execjs (2.3.0) + globalid (0.3.3) + activesupport (>= 4.1.0) + hike (1.2.3) + hitimes (1.2.2) + i18n (0.7.0) + json (1.8.2) + kindlerb (0.1.1) + mustache + nokogiri + loofah (2.0.1) + nokogiri (>= 1.5.9) + mail (2.6.3) + mime-types (>= 1.16, < 3) + metaclass (0.0.4) + 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.10.1) + mustache (1.0.0) + mysql (2.9.1) + mysql2 (0.3.18) + nokogiri (1.6.6.2) + 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.5) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.1) + loofah (~> 2.0) + rake (10.4.2) + rdoc (4.2.0) + redcarpet (3.2.2) + 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) + ruby-prof (0.11.3) + 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 (0.1.1.pre) + bunny (~> 1.1.3) + serverengine + thor + thread + sprockets (2.12.3) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.2.4) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.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.5) + thread_safe (0.3.4) + 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 + +DEPENDENCIES + activerecord-jdbcmysql-adapter (>= 1.3.0) + activerecord-jdbcpostgresql-adapter (>= 1.3.0) + activerecord-jdbcsqlite3-adapter (>= 1.3.0) + arel! + backburner + bcrypt (~> 3.1.7) + benchmark-ips + coffee-rails (~> 4.1.0) + dalli (>= 2.2.1) + delayed_job + jquery-rails! + json + kindlerb (= 0.1.1) + 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.2) + resque + resque-scheduler + ruby-prof (~> 0.11.2) + sdoc (~> 0.4.0) + sequel + sidekiq + sneakers (= 0.1.1.pre) + sqlite3 (~> 1.3.6) + stackprof + sucker_punch + turbolinks + uglifier (>= 1.3.0) + w3c_validators diff --git a/RAILS_VERSION b/RAILS_VERSION index e33286503d..2b915d7d5c 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -4.2.0.beta1 +5.0.0.alpha @@ -13,10 +13,10 @@ Person, Post, etc.) and encapsulates the business logic that is specific to your application. In Rails, database-backed model classes are derived from `ActiveRecord::Base`. Active Record allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. Although most Rails models are backed by a database, models can also -be ordinary Ruby classes, or Ruby classes that implement a set of interfaces -as provided by the Active Model module. You can read more about Active Record -in its [README](activerecord/README.rdoc). +methods. You can read more about Active Record in its [README](activerecord/README.rdoc). +Although most Rails models are backed by a database, models can also be ordinary +Ruby classes, or Ruby classes that implement a set of interfaces as provided by +the Active Model module. You can read more about Active Model in its [README](activemodel/README.rdoc). The _Controller layer_ is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning HTML, but Rails controllers @@ -36,9 +36,11 @@ You can read more about Action View in its [README](actionview/README.rdoc). Active Record, Action Pack, and Action View can each be used independently outside Rails. In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library -to generate and send emails; and Active Support ([README](activesupport/README.rdoc)), a collection of -utility classes and standard library extensions that are useful for Rails, and may also be used -independently outside Rails. +to generate and send emails; Active Job ([README](activejob/README.md)), a +framework for declaring jobs and making them run on a variety of queueing +backends; and Active Support ([README](activesupport/README.rdoc)), a collection +of utility classes and standard library extensions that are useful for Rails, +and may also be used independently outside Rails. ## Getting Started @@ -76,7 +78,7 @@ We encourage you to contribute to Ruby on Rails! Please check out the ## Code Status -* [![Build Status](https://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/rails/rails) +[![Build Status](https://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/rails/rails) ## License diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index f8c40f0b02..0e09234b82 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -25,18 +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 then 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 -* https://github.com/rails/activerecord-deprecated_finders - -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. @@ -54,7 +42,7 @@ addressed, and that can impact your release date. Ruby implementors have high stakes in making sure Rails works. Be kind and give them a heads up that Rails will be released soonish. -This only needs done for major and minor releases, bugfix releases aren't a +This is only required for major and minor releases, bugfix releases aren't a big enough deal, and are supposed to be backwards compatible. Send an email just giving a heads up about the upcoming release to these diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 1ff4db41a3..86ecb3ee88 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,44 +1,56 @@ -* Added #deliver_later, #deliver_now and deprecate #deliver in favour of - #deliver_now. #deliver_later will enqueue a job to render and deliver - the mail instead of delivering it right at that moment. The job is enqueued - using the new Active Job framework in Rails, and will use whatever queue is - configured for Rails. +* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. - *DHH/Abdelkader Boudih/Cristian Bica* + Example: -* Make ActionMailer::Previews methods class methods. Previously they were - instance methods and ActionMailer tries to render a message when they - are called. + def test_emails + assert_enqueued_emails 2 do + ContactMailer.welcome.deliver_later + ContactMailer.welcome.deliver_later + end + end - *Cristian Bica* + def test_no_emails + assert_no_enqueued_emails do + # No emails enqueued here + end + end -* Deprecate `*_path` helpers in email views. When used they generate - non-working links and are not the intention of most developers. Instead - we recommend to use `*_url` helper. + *George Claghorn* - *Richard Schneeman* +* Add `_mailer` suffix to mailers created via generator, following the same + naming convention used in controllers and jobs. -* Raise an exception when attachments are added after `mail` was called. - This is a safeguard to prevent invalid emails. + *Carlos Souza* - Fixes #16163. +* Remove deprecate `*_path` helpers in email views. - *Yves Senn* + *Rafael Mendonça França* -* Add `config.action_mailer.show_previews` configuration option. +* Remove deprecated `deliver` and `deliver!` methods. - This config option can be used to enable the mail preview in environments - other than development (such as staging). + *claudiob* - Defaults to `true` in development and false elsewhere. +* Template lookup now respects default locale and I18n fallbacks. - *Leonard Garvey* + Given the following templates: -* Allow preview interceptors to be registered through - `config.action_mailer.preview_interceptors`. + mailer/demo.html.erb + mailer/demo.en.html.erb + mailer/demo.pt.html.erb - See #15739. + Before this change, for a locale that doesn't have its associated file, the + `mailer/demo.html.erb` would be rendered even if `en` was the default locale. - *Yves Senn* + Now `mailer/demo.en.html.erb` has precedence over the file without locale. -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) for previous changes. + Also, it is possible to give a fallback. + + mailer/demo.pt.html.erb + mailer/demo.pt-BR.html.erb + + So if the locale is `pt-PT`, `mailer/demo.pt.html.erb` will be rendered given + the right I18n fallback configuration. + + *Rafael Mendonça França* + +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index d58dd9ed9b..3ec7a617cf 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 3b8e3ed749..a4e660d621 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -61,7 +61,7 @@ generated would look like this: Thank you for signing up! -In order to send mails, you simply call the method and then call +deliver+ on the return value. +In order to send mails, you simply call the method and then call +deliver_now+ on the return value. Calling the method returns a Mail Message object: @@ -74,9 +74,17 @@ Or you can just chain the methods together like: == Setting defaults -It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from <tt>ActionMailer::Base</tt>. This method accepts a Hash as the parameter. You can use any of the headers email messages have, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed. +It is possible to set default values that will be used in every method in your +Action Mailer class. To implement this functionality, you just call the public +class method +default+ which you get for free from <tt>ActionMailer::Base</tt>. +This method accepts a Hash as the parameter. You can use any of the headers, +email messages have, like +:from+ as the key. You can also pass in a string as +the key, like "Content-Type", but Action Mailer does this out of the box for you, +so you won't need to worry about that. Finally, it is also possible to pass in a +Proc that will get evaluated when it is needed. -Note that every value you set with this method will get overwritten if you use the same key in your mailer method. +Note that every value you set with this method will get overwritten if you use the +same key in your mailer method. Example: @@ -87,10 +95,11 @@ Example: == Receiving emails -To receive emails, you need to implement a public instance method called <tt>receive</tt> that takes an -email object as its single parameter. The Action Mailer framework has a corresponding class method, -which is also called <tt>receive</tt>, that accepts a raw, unprocessed email as a string, which it then turns -into the email object and calls the receive instance method. +To receive emails, you need to implement a public instance method called ++receive+ that takes an email object as its single parameter. The Action Mailer +framework has a corresponding class method, which is also called +receive+, that +accepts a raw, unprocessed email as a string, which it then turns into the email +object and calls the receive instance method. Example: @@ -111,13 +120,14 @@ Example: end end -This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the -trivial case like this: +This Mailman can be the target for Postfix or other MTAs. In Rails, you would use +the runner in the trivial case like this: rails runner 'Mailman.receive(STDIN.read)' -However, invoking Rails in the runner for each mail to be received is very resource intensive. A single -instance of Rails should be run within a daemon, if it is going to process more than just a limited amount of email. +However, invoking Rails in the runner for each mail to be received is very +resource intensive. A single instance of Rails should be run within a daemon, if +it is going to process more than just a limited amount of email. == Configuration diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index 5ddd90020b..8f4de8fafa 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -10,6 +10,7 @@ Rake::TestTask.new { |t| t.pattern = 'test/**/*_test.rb' t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) } namespace :test do diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 089e12ebf1..513c217733 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activejob', version s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4'] - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2' + s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' end diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index b994ef3182..17d8dcc208 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f539fc53c6..3e5125f72e 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -15,11 +15,17 @@ module ActionMailer # # $ rails generate mailer Notifier # - # The generated model inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods + # The generated model inherits from <tt>ApplicationMailer</tt> which in turn + # inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods # used to generate an email message. In these methods, you can setup variables to be used in # the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments. # - # class Notifier < ActionMailer::Base + # class ApplicationMailer < ActionMailer::Base + # default from: 'from@exmaple.com' + # layout 'mailer' + # end + # + # class NotifierMailer < ApplicationMailer # default from: 'no-reply@example.com', # return_path: 'system@example.com' # @@ -39,11 +45,8 @@ module ActionMailer # in the same manner as <tt>attachments[]=</tt> # # * <tt>headers[]=</tt> - Allows you to specify any header field in your email such - # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note, while most fields like <tt>To:</tt> - # <tt>From:</tt> can only appear once in an email header, other fields like <tt>X-Anything</tt> - # can appear multiple times. If you want to change a field that can appear multiple times, - # you need to set it to nil first so that Mail knows you are replacing it and not adding - # another field of the same name. + # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times + # will add many fields of the same name. Read #headers doc for more information. # # * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such # as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt> @@ -55,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: # @@ -85,9 +88,10 @@ 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 model are accessible as instance variables in the view. + # Variables defined in the methods of your mailer model are accessible as instance variables in their + # corresponding view. # # Emails by default are sent in plain text, so a sample view for our model example might look like this: # @@ -128,30 +132,25 @@ module ActionMailer # # config.action_mailer.default_url_options = { host: "example.com" } # - # When you decide to set a default <tt>:host</tt> for your mailers, then you need to make sure to use the - # <tt>only_path: false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper - # will generate relative URLs by default when a <tt>:host</tt> option isn't explicitly provided, passing - # <tt>only_path: false</tt> will ensure that absolute URLs are generated. - # # = Sending mail # # Once a mailer action and template are defined, you can deliver your message or create it and save it # for delivery later: # - # Notifier.welcome(User.first).deliver_now # sends the email - # mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object + # NotifierMailer.welcome(User.first).deliver_now # sends the email + # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object # mail.deliver_now # sends the email # # 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. # - # 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): # - # 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 # # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. # @@ -180,7 +179,7 @@ module ActionMailer # # Sending attachment in emails is easy: # - # class ApplicationMailer < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # def welcome(recipient) # attachments['free_book.pdf'] = File.read('path/to/file.pdf') # mail(to: recipient, subject: "New account information") @@ -196,7 +195,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 ApplicationMailer < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # def welcome(recipient) # attachments['free_book.pdf'] = File.read('path/to/file.pdf') # mail(to: recipient, subject: "New account information", body: "") @@ -208,7 +207,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 ApplicationMailer < ActionMailer::Base + # 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") @@ -247,7 +246,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 < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # default sender: 'system@example.com' # end # @@ -265,7 +264,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 < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # default 'Content-Transfer-Encoding' => '7bit', # content_description: 'This is a description' # end @@ -273,7 +272,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 < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # default 'X-Special-Header' => Proc.new { my_method } # # private @@ -298,7 +297,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 < ActionMailer::Base + # class NotifierMailer < ApplicationMailer # before_action :add_inline_attachment! # # def welcome @@ -326,9 +325,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 # @@ -494,7 +493,7 @@ module ActionMailer # Sets the defaults through app configuration: # - # config.action_mailer.default { from: "no-reply@example.org" } + # config.action_mailer.default(from: "no-reply@example.org") # # Aliased by ::default_options= def default(value = nil) @@ -587,8 +586,6 @@ module ActionMailer } ActiveSupport::Notifications.instrument("process.action_mailer", payload) do - lookup_context.skip_default_locale! - super @_message = NullMail.new unless @_mail_was_called end @@ -625,6 +622,26 @@ module ActionMailer # The resulting <tt>Mail::Message</tt> will have the following in its header: # # X-Special-Domain-Specific-Header: SecretValue + # + # Note about replacing already defined headers: + # + # * +subject+ + # * +sender+ + # * +from+ + # * +to+ + # * +cc+ + # * +bcc+ + # * +reply-to+ + # * +orig-date+ + # * +message-id+ + # * +references+ + # + # Fields can only appear once in email headers while other fields such as + # <tt>X-Anything</tt> can appear multiple times. + # + # If you want to replace any header which already exists, first set it to + # +nil+ in order to reset the value otherwise another field will be added + # for the same header. def headers(args = nil) if args @_message.headers(args) @@ -685,8 +702,8 @@ module ActionMailer # The main method that creates the message and renders the email templates. There are # two ways to call this method, with a block, or without a block. # - # Both methods accept a headers hash. This hash allows you to specify the most used headers - # in an email message, these are: + # It accepts a headers hash. This hash allows you to specify + # the most used headers in an email message, these are: # # * +:subject+ - The subject of the message, if this is omitted, Action Mailer will # ask the Rails I18n class for a translated +:subject+ in the scope of @@ -775,7 +792,6 @@ module ActionMailer def mail(headers = {}, &block) return @_message if @_mail_was_called && headers.blank? && !block - @_mail_was_called = true m = @_message # At the beginning, do not consider class default for content_type @@ -803,6 +819,8 @@ module ActionMailer # Render the templates and blocks responses = collect_responses(headers, &block) + @_mail_was_called = true + create_parts_from_responses(m, responses) # Setup content type, reapply charset and handle parts order @@ -834,7 +852,7 @@ module ActionMailer when user_content_type.present? user_content_type when m.has_attachments? - if m.attachments.detect { |a| a.inline? } + if m.attachments.detect(&:inline?) ["multipart", "related", params] else ["multipart", "mixed", params] @@ -889,7 +907,7 @@ module ActionMailer if templates.empty? raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer') else - templates.uniq { |t| t.formats }.each(&block) + templates.uniq(&:formats).each(&block) end end diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb index 622f202695..e864ab7a4d 100644 --- a/actionmailer/lib/action_mailer/delivery_job.rb +++ b/actionmailer/lib/action_mailer/delivery_job.rb @@ -1,10 +1,12 @@ require 'active_job' module ActionMailer - class DeliveryJob < ActiveJob::Base #:nodoc: + # 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 - def perform(mailer, mail_method, delivery_method, *args) #:nodoc# + def perform(mailer, mail_method, delivery_method, *args) #:nodoc: mailer.constantize.public_send(mail_method, *args).send(delivery_method) end end diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index b9162055f7..ac79788cf0 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -5,10 +5,10 @@ module ActionMailer end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index 483277af04..227192f48d 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -29,10 +29,12 @@ module ActionMailer # Access the message attachments list. def attachments - @_message.attachments + mailer.attachments 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/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 7118f9b02c..ff2cb0fd01 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -46,6 +46,7 @@ module ActionMailer # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time + # * <tt>:queue</tt> - Enqueue the email on the specified queue def deliver_later!(options={}) enqueue_delivery :deliver_now!, options end @@ -61,6 +62,7 @@ module ActionMailer # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time + # * <tt>:queue</tt> - Enqueue the email on the specified queue def deliver_later(options={}) enqueue_delivery :deliver_now, options end @@ -82,18 +84,6 @@ module ActionMailer message.deliver end - def deliver! #:nodoc: - ActiveSupport::Deprecation.warn "#deliver! is deprecated and will be removed in Rails 5. " \ - "Use #deliver_now! to deliver immediately or #deliver_later! to deliver through Active Job." - deliver_now! - end - - def deliver #:nodoc: - ActiveSupport::Deprecation.warn "#deliver is deprecated and will be removed in Rails 5. " \ - "Use #deliver_now to deliver immediately or #deliver_later to deliver through Active Job." - deliver_now - end - private def enqueue_delivery(delivery_method, options={}) diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 05a3ac6a21..bebcf4de01 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -41,7 +41,7 @@ module ActionMailer options.each { |k,v| send("#{k}=", v) } if options.show_previews - app.routes.append do + app.routes.prepend do get '/rails/mailers' => "rails/mailers#index" get '/rails/mailers/*path' => "rails/mailers#preview" end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index b53d2b7fa0..766215ce96 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -17,6 +17,7 @@ module ActionMailer include ActiveSupport::Testing::ConstantLookup include TestHelper include Rails::Dom::Testing::Assertions::SelectorAssertions + include Rails::Dom::Testing::Assertions::DomAssertions included do class_attribute :_mailer_class diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 6ddacf7b79..524e6e3af1 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 @@ -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 d5bf864595..3ec7d3d896 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -4,13 +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/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb new file mode 100644 index 0000000000..d25d8892dd --- /dev/null +++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout 'mailer' +end diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb index edcfb4233d..348d314758 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb @@ -1,12 +1,11 @@ <% module_namespacing do -%> -class <%= class_name %> < ActionMailer::Base - default from: "from@example.com" +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..c4d5cb8c33 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -15,7 +15,7 @@ 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 diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb index dd3b38544f..10cfdcf693 100644 --- a/actionmailer/test/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -22,7 +22,7 @@ class AssetHostTest < ActionMailer::TestCase def test_asset_host_as_string mail = AssetHostMailer.email_with_asset - assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip + assert_dom_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip end def test_asset_host_as_one_argument_proc @@ -32,6 +32,6 @@ class AssetHostTest < ActionMailer::TestCase end } mail = AssetHostMailer.email_with_asset - assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip + assert_dom_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index d00f5aea47..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' @@ -10,6 +9,8 @@ require 'mailers/proc_mailer' require 'mailers/asset_mailer' class BaseTest < ActiveSupport::TestCase + include Rails::Dom::Testing::Assertions::DomAssertions + setup do @original_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.delivery_method = :test @@ -259,6 +260,20 @@ class BaseTest < ActiveSupport::TestCase assert_match(/Can't add attachments after `mail` was called./, e.message) end + test "adding inline attachments while rendering mail works" do + class LateInlineAttachmentMailer < ActionMailer::Base + def on_render + mail from: "welcome@example.com", to: "to@example.com" + end + end + + mail = LateInlineAttachmentMailer.on_render + assert_nothing_raised { mail.message } + + assert_equal ["image/jpeg; filename=controller_attachments.jpg", + "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map {|a| a['Content-Type'].to_s } + end + test "accessing attachments works after mail was called" do class LateAttachmentAccessorMailer < ActionMailer::Base def welcome @@ -271,7 +286,7 @@ class BaseTest < ActiveSupport::TestCase end end - assert_nothing_raised { LateAttachmentAccessorMailer.welcome } + assert_nothing_raised { LateAttachmentAccessorMailer.welcome.message } end # Implicit multipart @@ -337,10 +352,35 @@ class BaseTest < ActiveSupport::TestCase assert_equal("text/plain", email.parts[0].mime_type) assert_equal("Implicit with locale PL TEXT", email.parts[0].body.encoded) assert_equal("text/html", email.parts[1].mime_type) - assert_equal("Implicit with locale HTML", email.parts[1].body.encoded) + assert_equal("Implicit with locale EN HTML", email.parts[1].body.encoded) end end + test "implicit multipart with fallback locale" do + fallback_backend = Class.new(I18n::Backend::Simple) do + include I18n::Backend::Fallbacks + end + + begin + backend = I18n.backend + I18n.backend = fallback_backend.new + I18n.fallbacks[:"de-AT"] = [:de] + + swap I18n, locale: 'de-AT' do + email = BaseMailer.implicit_with_locale + assert_equal(2, email.parts.size) + assert_equal("multipart/alternative", email.mime_type) + assert_equal("text/plain", email.parts[0].mime_type) + assert_equal("Implicit with locale DE-AT TEXT", email.parts[0].body.encoded) + assert_equal("text/html", email.parts[1].mime_type) + assert_equal("Implicit with locale DE HTML", email.parts[1].body.encoded) + end + ensure + I18n.backend = backend + end + end + + test "implicit multipart with several view paths uses the first one with template" do old = BaseMailer.view_paths begin @@ -522,7 +562,7 @@ class BaseTest < ActiveSupport::TestCase mail = AssetMailer.welcome - assert_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip) + assert_dom_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip) end test "assets tags should use a Mailer's asset_host settings when available" do @@ -536,7 +576,7 @@ class BaseTest < ActiveSupport::TestCase mail = TempAssetMailer.welcome - assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) + assert_dom_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) end test 'the view is not rendered when mail was never called' do diff --git a/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb new file mode 100644 index 0000000000..e97505fad9 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb @@ -0,0 +1 @@ +Implicit with locale DE-AT TEXT
\ No newline at end of file diff --git a/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb new file mode 100644 index 0000000000..0536b5d3e2 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb @@ -0,0 +1 @@ +Implicit with locale DE HTML
\ No newline at end of file diff --git a/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb new file mode 100644 index 0000000000..6decd3bb31 --- /dev/null +++ b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb @@ -0,0 +1,7 @@ +<h1>Adding an inline image while rendering</h1> + +<% controller.attachments.inline["controller_attachments.jpg"] = 'via controller.attachments.inline' %> +<%= image_tag attachments['controller_attachments.jpg'].url %> + +<% attachments.inline["attachments.jpg"] = 'via attachments.inline' %> +<%= image_tag attachments['attachments.jpg'].url %> diff --git a/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb new file mode 100644 index 0000000000..0322c1191e --- /dev/null +++ b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb @@ -0,0 +1 @@ +<%= url_for(@options) %> <%= @url %> diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index 9abf8b225c..e4dd269494 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_job' require 'minitest/mock' @@ -32,25 +31,6 @@ class MessageDeliveryTest < ActiveSupport::TestCase assert_equal Mail::Message , @mail.message.class end - test 'should respond to .deliver' do - assert_respond_to @mail, :deliver - end - - test 'should respond to .deliver!' do - assert_respond_to @mail, :deliver! - end - - test '.deliver is deprecated' do - assert_deprecated do - @mail.deliver - end - end - test '.deliver! is deprecated' do - assert_deprecated do - @mail.deliver! - end - end - test 'should respond to .deliver_later' do assert_respond_to @mail, :deliver_later end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 96b75ff2e0..089933e245 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 @@ -119,6 +121,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/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index e79b2ca978..7928fe9542 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -23,9 +23,32 @@ class UrlTestMailer < ActionMailer::Base mail(to: recipient, subject: "[Signed up] Welcome #{recipient}", from: "system@loudthinking.com", date: Time.local(2004, 12, 12)) end + + def exercise_url_for(options) + @options = options + @url = url_for(@options) + mail(from: 'from@example.com', to: 'to@example.com', subject: 'subject') + end end class ActionMailerUrlTest < ActionMailer::TestCase + class DummyModel + def self.model_name + OpenStruct.new(route_key: 'dummy_model') + end + + def persisted? + false + end + + def model_name + self.class.model_name + end + + def to_model + self + end + end def encode( text, charset="UTF-8" ) quoted_printable( text, charset ) @@ -40,10 +63,47 @@ class ActionMailerUrlTest < ActionMailer::TestCase mail end + def assert_url_for(expected, options, relative = false) + expected = "http://www.basecamphq.com#{expected}" if expected.start_with?('/') && !relative + urls = UrlTestMailer.exercise_url_for(options).body.to_s.chomp.split + + assert_equal expected, urls.first + assert_equal expected, urls.second + end + def setup @recipient = 'test@localhost' end + def test_url_for + UrlTestMailer.delivery_method = :test + + AppRoutes.draw do + get ':controller(/:action(/:id))' + get '/welcome' => 'foo#bar', as: 'welcome' + get '/dummy_model' => 'foo#baz', as: 'dummy_model' + end + + # string + assert_url_for 'http://foo/', 'http://foo/' + + # symbol + assert_url_for '/welcome', :welcome + + # hash + assert_url_for '/a/b/c', controller: 'a', action: 'b', id: 'c' + assert_url_for '/a/b/c', {controller: 'a', action: 'b', id: 'c', only_path: true}, true + + # model + assert_url_for '/dummy_model', DummyModel.new + + # class + assert_url_for '/dummy_model', DummyModel + + # array + assert_url_for '/dummy_model' , [DummyModel] + end + def test_signed_up_with_url UrlTestMailer.delivery_method = :test @@ -66,13 +126,13 @@ class ActionMailerUrlTest < ActionMailer::TestCase expected.message_id = '<123@456>' created.message_id = '<123@456>' - assert_equal expected.encoded, created.encoded + assert_dom_equal expected.encoded, created.encoded assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver_now } assert_not_nil ActionMailer::Base.deliveries.first delivered = ActionMailer::Base.deliveries.first delivered.message_id = '<123@456>' - assert_equal expected.encoded, delivered.encoded + assert_dom_equal expected.encoded, delivered.encoded end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index de9722c392..1083f4a92d 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,376 +1,252 @@ -* Deprecate implicit Array conversion for Response objects. It was added - (using `#to_ary`) so we could conveniently use implicit splatting: +* Fix regression in functional tests. Responses should have default headers + assigned. - status, headers, body = response + See #18423. - But it also means `response + response` works and `[response].flatten` - cascades down to the Rack body. Nonsense behavior. Instead, rely on - explicit conversion and splatting with `#to_a`: + *Jeremy Kemper*, *Yves Senn* - status, header, body = *response +* 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). - *Jeremy Kemper* + *Iain Beeston* -* Don't rescue `IPAddr::InvalidAddressError`. +* 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]`. - `IPAddr::InvalidAddressError` does not exist in Ruby 1.9.3 - and fails for JRuby in 1.9 mode. + *David Ilizarov* - *Peter Suschlik* +* Non-string authenticity tokens do not raise NoMethodError when decoding + the masked token. -* Fix bug where the router would ignore any constraints added to redirect - routes. + *Ville Lautanala* - Fixes #16605. +* Add `http_cache_forever` to Action Controller, so we can cache a response + that never gets expired. - *Agis Anastasopoulos* + *arthurnn* -* Allow `config.action_dispatch.trusted_proxies` to accept an IPAddr object. +* `ActionController#translate` supports symbols as shortcuts. + When shortcut is given it also lookups without action name. - Example: - - # config/environments/production.rb - config.action_dispatch.trusted_proxies = IPAddr.new('4.8.15.0/16') - - *Sam Aarons* - -* Avoid duplicating routes for HEAD requests. - - Instead of duplicating the routes, we will first match the HEAD request to - HEAD routes. If no match is found, we will then map the HEAD request to - GET routes. - - *Guo Xiang Tan*, *Andrew White* - -* Requests that hit `ActionDispatch::Static` can now take advantage - of gzipped assets on disk. By default a gzip asset will be served if - the client supports gzip and a compressed file is on disk. - - *Richard Schneeman* - -* `ActionController::Parameters` will stop inheriting from `Hash` and - `HashWithIndifferentAccess` in the next major release. If you use any method - that is not available on `ActionController::Parameters` you should consider - calling `#to_h` to convert it to a `Hash` first before calling that method. - - *Prem Sichanugrist* - -* `ActionController::Parameters#to_h` now returns a `Hash` with unpermitted - keys removed. This change is to reflect on a security concern where some - method performed on an `ActionController::Parameters` may yield a `Hash` - object which does not maintain `permitted?` status. If you would like to - get a `Hash` with all the keys intact, duplicate and mark it as permitted - before calling `#to_h`. - - params = ActionController::Parameters.new({ - name: 'Senjougahara Hitagi', - oddity: 'Heavy stone crab' - }) - params.to_h - # => {} - - unsafe_params = params.dup.permit! - unsafe_params.to_h - # => {"name"=>"Senjougahara Hitagi", "oddity"=>"Heavy stone crab"} - - safe_params = params.permit(:name) - safe_params.to_h - # => {"name"=>"Senjougahara Hitagi"} - - This change is consider a stopgap as we cannot change the code to stop - `ActionController::Parameters` to inherit from `HashWithIndifferentAccess` - in the next minor release. - - *Prem Sichanugrist* - -* Deprecated `TagAssertions`. - - *Kasper Timm Hansen* - -* Use the Active Support JSON encoder for cookie jars using the `:json` or - `:hybrid` serializer. This allows you to serialize custom Ruby objects into - cookies by defining the `#as_json` hook on such objects. - - Fixes #16520. - - *Godfrey Chan* - -* Add `config.action_dispatch.cookies_digest` option for setting custom - digest. The default remains the same - 'SHA1'. - - *Łukasz Strzałkowski* - -* Move `respond_with` (and the class-level `respond_to`) to - the `responders` gem. - - *José Valim* - -* When your templates change, browser caches bust automatically. - - New default: the template digest is automatically included in your ETags. - When you call `fresh_when @post`, the digest for `posts/show.html.erb` - is mixed in so future changes to the HTML will blow HTTP caches for you. - This makes it easy to HTTP-cache many more of your actions. - - If you render a different template, you can now pass the `:template` - option to include its digest instead: - - fresh_when @post, template: 'widgets/show' - - Pass `template: false` to skip the lookup. To turn this off entirely, set: - - config.action_controller.etag_with_template_digest = false + *Max Melentiev* - *Jeremy Kemper* +* 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. -* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` - in favor of `AbstractController::Helpers::MissingHelperError`. + # Before + def index + @articles = Article.all + fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at)) + end - *Yves Senn* + # After + def index + @articles = Article.all + fresh_when(@articles) + end -* Fix `assert_template` not being able to assert that no files were rendered. + *claudiob* - *Guo Xiang Tan* +* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback -* Extract source code for the entire exception stack trace for - better debugging and diagnosis. + 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. - *Ryan Dao* - -* Allows ActionDispatch::Request::LOCALHOST to match any IPv4 127.0.0.0/8 - loopback address. - - *Earl St Sauver*, *Sven Riedel* - -* Preserve original path in `ShowExceptions` middleware by stashing it as - `env["action_dispatch.original_path"]` - - `ActionDispatch::ShowExceptions` overwrites `PATH_INFO` with the status code - for the exception defined in `ExceptionWrapper`, so the path - the user was visiting when an exception occurred was not previously - available to any custom exceptions_app. The original `PATH_INFO` is now - stashed in `env["action_dispatch.original_path"]`. - - *Grey Baker* - -* Use `String#bytesize` instead of `String#size` when checking for cookie - overflow. - - *Agis Anastasopoulos* - -* `render nothing: true` or rendering a `nil` body no longer add a single - space to the response body. - - The old behavior was added as a workaround for a bug in an early version of - Safari, where the HTTP headers are not returned correctly if the response - body has a 0-length. This is been fixed since and the workaround is no - longer necessary. - - Use `render body: ' '` if the old behavior is desired. - - See #14883 for details. - - *Godfrey Chan* - -* Prepend a JS comment to JSONP callbacks. Addresses CVE-2014-4671 - ("Rosetta Flash"). - - *Greg Campbell* - -* Because URI paths may contain non US-ASCII characters we need to force - the encoding of any unescaped URIs to UTF-8 if they are US-ASCII. - This essentially replicates the functionality of the monkey patch to - URI.parser.unescape in active_support/core_ext/uri.rb. - - Fixes #16104. - - *Karl Entwistle* + Example: -* Generate shallow paths for all children of shallow resources. + draw do + get '/home' => 'test#index' + mount rack_app, at: '/' + end + head '/home' + assert_response :success - Fixes #15783. + 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. - *Seb Jacobs* + *Terence Sun* -* JSONP responses are now rendered with the `text/javascript` content type - when rendering through a `respond_to` block. +* Migrating xhr methods to keyword arguments syntax + in `ActionController::TestCase` and `ActionDispatch::Integration` - Fixes #15081. + Old syntax: - *Lucas Mazza* + xhr :get, :create, params: { id: 1 } -* Add `config.action_controller.always_permitted_parameters` to configure which - parameters are permitted globally. The default value of this configuration is - `['controller', 'action']`. + New syntax example: - *Gary S. Weaver*, *Rafael Chacon* + get :create, params: { id: 1 }, xhr: true -* Fix env['PATH_INFO'] missing leading slash when a rack app mounted at '/'. + *Kir Shatrov* - Fixes #15511. +* Migrating to keyword arguments syntax in `ActionController::TestCase` and + `ActionDispatch::Integration` HTTP request methods. - *Larry Lv* + Example: -* ActionController::Parameters#require now accepts `false` values. + post :create, params: { y: x }, session: { a: 'b' } + get :view, params: { id: 1 } + get :view, params: { id: 1 }, format: :json - Fixes #15685. + *Kir Shatrov* - *Sergio Romano* +* Preserve default url options when generating URLs. -* With authorization header `Authorization: Token token=`, `authenticate` now - recognize token as nil, instead of "token". + 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. - Fixes #14846. + *Tekin Suleyman* - *Larry Lv* +* Deprecate *_via_redirect integration test methods. -* Ensure the controller is always notified as soon as the client disconnects - during live streaming, even when the controller is blocked on a write. + Use `follow_redirect!` manually after the request call for the same behavior. - *Nicholas Jakobsen*, *Matthew Draper* + *Aditya Kapoor* -* Routes specifying 'to:' must be a string that contains a "#" or a rack - application. Use of a symbol should be replaced with `action: symbol`. - Use of a string without a "#" should be replaced with `controller: string`. +* Add `ActionController::Renderer` to render arbitrary templates + outside controller actions. - *Aaron Patterson* + Its functionality is accessible through class methods `render` and + `renderer` of `ActionController::Base`. -* Fix URL generation with `:trailing_slash` such that it does not add - a trailing slash after `.:format` + *Ravil Bayramgalin* - *Dan Langevin* +* Support `:assigns` option when rendering with controllers/mailers. -* Build full URI as string when processing path in integration tests for - performance reasons. + *Ravil Bayramgalin* - *Guo Xiang Tan* +* Default headers, removed in controller actions, are no longer reapplied on + the test response. -* Fix `'Stack level too deep'` when rendering `head :ok` in an action method - called 'status' in a controller. + *Jonas Baumann* - Fixes #13905. +* Deprecate all *_filter callbacks in favor of *_action callbacks. - *Christiaan Van den Poel* + *Rafael Mendonça França* -* Add MKCALENDAR HTTP method (RFC 4791). +* 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. - *Sergey Karpesh* + Example: -* Instrument fragment cache metrics. + class ApplicationController < ActionController::Base + before_action :authenticate + protect_from_forgery prepend: false, unless: -> { @authenticated_by.oauth? } - Adds `:controller`: and `:action` keys to the instrumentation payload - for the `*_fragment.action_controller` notifications. This allows tracking - e.g. the fragment cache hit rates for each controller action. + private + def authenticate + if oauth_request? + # authenticate with oauth + @authenticated_by = 'oauth'.inquiry + else + # authenticate with cookies + @authenticated_by = 'cookie'.inquiry + end + end + end - *Daniel Schierbeck* + *Josef Šimánek* -* Always use the provided port if the protocol is relative. +* Remove `ActionController::HideActions`. - Fixes #15043. + *Ravil Bayramgalin* - *Guilherme Cavalcanti*, *Andrew White* +* Remove `respond_to`/`respond_with` placeholder methods, this functionality + has been extracted to the `responders` gem. -* Moved `params[request_forgery_protection_token]` into its own method - and improved tests. + *Carlos Antonio da Silva* - Fixes #11316. +* Remove deprecated assertion files. - *Tom Kadwill* + *Rafael Mendonça França* -* Added verification of route constraints given as a Proc or an object responding - to `:matches?`. Previously, when given an non-complying object, it would just - silently fail to enforce the constraint. It will now raise an `ArgumentError` - when setting up the routes. +* Remove deprecated usage of string keys in URL helpers. - *Xavier Defrang* + *Rafael Mendonça França* -* Properly treat the entire IPv6 User Local Address space as private for - purposes of remote IP detection. Also handle uppercase private IPv6 - addresses. +* Remove deprecated `only_path` option on `*_path` helpers. - Fixes #12638. + *Rafael Mendonça França* - *Caleb Spare* +* Remove deprecated `NamedRouteCollection#helpers`. -* Fixed an issue with migrating legacy json cookies. + *Rafael Mendonça França* - Previously, the `VerifyAndUpgradeLegacySignedMessage` assumes all incoming - cookies are marshal-encoded. This is not the case when `secret_token` is - used in conjunction with the `:json` or `:hybrid` serializer. +* Remove deprecated support to define routes with `:to` option that doesn't contain `#`. - In those case, when upgrading to use `secret_key_base`, this would cause a - `TypeError: incompatible marshal file format` and a 500 error for the user. + *Rafael Mendonça França* - Fixes #14774. +* Remove deprecated `ActionDispatch::Response#to_ary`. - *Godfrey Chan* + *Rafael Mendonça França* -* Make URL escaping more consistent: +* Remove deprecated `ActionDispatch::Request#deep_munge`. - 1. Escape '%' characters in URLs - only unescaped data should be passed to URL helpers - 2. Add an `escape_segment` helper to `Router::Utils` that escapes '/' characters - 3. Use `escape_segment` rather than `escape_fragment` in optimized URL generation - 4. Use `escape_segment` rather than `escape_path` in URL generation + *Rafael Mendonça França* - For point 4 there are two exceptions. Firstly, when a route uses wildcard segments - (e.g. `*foo`) then we use `escape_path` as the value may contain '/' characters. This - means that wildcard routes can't be optimized. Secondly, if a `:controller` segment - is used in the path then this uses `escape_path` as the controller may be namespaced. +* Remove deprecated `ActionDispatch::Http::Parameters#symbolized_path_parameters`. - Fixes #14629, #14636 and #14070. + *Rafael Mendonça França* - *Andrew White*, *Edho Arief* +* Remove deprecated option `use_route` in controller tests. -* Add alias `ActionDispatch::Http::UploadedFile#to_io` to - `ActionDispatch::Http::UploadedFile#tempfile`. + *Rafael Mendonça França* - *Tim Linquist* +* Ensure `append_info_to_payload` is called even if an exception is raised. -* Returns null type format when format is not know and controller is using `any` - format block. + Fixes an issue where when an exception is raised in the request the additonal + payload data is not available. - Fixes #14462. + See: + * #14903 + * https://github.com/roidrage/lograge/issues/37 - *Rafael Mendonça França* + *Dieter Komendera*, *Margus Pärt* -* Improve routing error page with fuzzy matching search. +* Correctly rely on the response's status code to handle calls to `head`. - *Winston* + *Robin Dupret* -* Only make deeply nested routes shallow when parent is shallow. +* Using `head` method returns empty response_body instead + of returning a single space " ". - Fixes #14684. + The old behavior was added as a workaround for a bug in an early + version of Safari, where the HTTP headers are not returned correctly + if the response body has a 0-length. This is been fixed since and + the workaround is no longer necessary. - *Andrew White*, *James Coglan* + Fixes #18253. -* Append link to bad code to backtrace when exception is `SyntaxError`. + *Prathamesh Sonpatki* - *Boris Kuznetsov* +* Fix how polymorphic routes works with objects that implement `to_model`. -* Swapped the parameters of assert_equal in `assert_select` so that the - proper values were printed correctly. + *Travis Grathwell* - Fixes #14422. +* Stop converting empty arrays in `params` to `nil`. - *Vishal Lal* + This behaviour was introduced in response to CVE-2012-2660, CVE-2012-2694 + and CVE-2013-0155 -* The method `shallow?` returns false if the parent resource is a singleton so - we need to check if we're not inside a nested scope before copying the :path - and :as options to their shallow equivalents. + ActiveRecord now issues a safe query when passing an empty array into + a where clause, so there is no longer a need to defend against this type + of input (any nils are still stripped from the array). - Fixes #14388. + *Chris Sinjakli* - *Andrew White* +* Fixed usage of optional scopes in url helpers. -* Make logging of CSRF failures optional (but on by default) with the - `log_warning_on_csrf_failure` configuration setting in - `ActionController::RequestForgeryProtection`. + *Alex Robbin* - *John Barton* +* Fixed handling of positional url helper arguments when `format: false`. -* Fix URL generation in controller tests with request-dependent - `default_url_options` methods. + Fixes #17819. - *Tony Wooster* + *Andrew White*, *Tatiana Soukiassian* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index d58dd9ed9b..3ec7a617cf 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc deleted file mode 100644 index f96a9d9da5..0000000000 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ /dev/null @@ -1,17 +0,0 @@ -== Running with Rake - -The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all classes. For more information, check out the -full array of rake tasks with "rake -T". - -Rake can be found at http://docs.seattlerb.org/rake/. - -== Running by hand - -Run a single test suite: - - rake test TEST=path/to/test.rb - -Run one test in a test suite: - - rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" diff --git a/actionpack/Rakefile b/actionpack/Rakefile index af075f82ad..4b60b77759 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -16,6 +16,7 @@ Rake::TestTask.new do |t| t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end namespace :test do diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index c752388e28..d9b23ad4a9 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -21,10 +21,10 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.6.0.beta' + s.add_dependency 'rack', '~> 1.6' s.add_dependency 'rack-test', '~> 0.6.2' - s.add_dependency 'rails-html-sanitizer', '~> 1.0' - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_dependency 'actionview', version s.add_development_dependency 'activemodel', version diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 4026dab2ce..c95b9a4097 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -12,7 +12,7 @@ module AbstractController class ActionNotFound < StandardError end - # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be + # AbstractController::Base is a low-level API. Nobody should be # using it directly, and subclasses (like ActionController::Base) are # expected to provide their own +render+ method, since rendering means # different things depending on the context. @@ -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 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,25 +72,26 @@ 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 { |x| x.to_s } - - # And always exclude explicitly hidden actions - hidden_actions.to_a + public_instance_methods(false)).uniq.map(&:to_s) - # Clear out AS callback method pollution - Set.new(methods.reject { |method| method =~ /_one_time_conditions/ }) + methods.to_set end end # action_methods are cached and there is sometimes need to refresh - # them. clear_action_methods! allows you to do that, so next time + # them. ::clear_action_methods! allows you to do that, so next time # you run action_methods, they will be recalculated def clear_action_methods! @action_methods = nil end # Returns the full controller name, underscored, without the ending Controller. - # For instance, MyApp::MyPostsController would return "my_app/my_posts" for - # controller_path. + # + # class MyApp::MyPostsController < AbstractController::Base + # end + # end + # + # MyApp::MyPostsController.controller_path # => "my_app/my_posts" # # ==== Returns # * <tt>String</tt> @@ -137,12 +128,12 @@ module AbstractController process_action(action_name, *args) end - # Delegates to the class' #controller_path + # Delegates to the class' ::controller_path def controller_path self.class.controller_path end - # Delegates to the class' #action_methods + # Delegates to the class' ::action_methods def action_methods self.class.action_methods end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index ca5c80cd71..f4fd1db36c 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module AbstractController module Callbacks extend ActiveSupport::Concern @@ -9,7 +11,7 @@ module AbstractController included do define_callbacks :process_action, - terminator: ->(controller,_) { controller.response_body }, + terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body }, skip_after_callbacks_if_terminated: true end @@ -22,10 +24,21 @@ module AbstractController end module ClassMethods - # If :only or :except are used, convert the options into the - # :unless and :if options of ActiveSupport::Callbacks. - # The basic idea is that :only => :index gets converted to - # :if => proc {|c| c.action_name == "index" }. + # If +:only+ or +:except+ are used, convert the options into the + # +:if+ and +:unless+ options of ActiveSupport::Callbacks. + # + # 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 @@ -50,11 +63,16 @@ module AbstractController # impossible to skip a callback defined using an anonymous proc # using #skip_action_callback def skip_action_callback(*names) + ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.') skip_before_action(*names) skip_after_action(*names) skip_around_action(*names) end - alias_method :skip_filter, :skip_action_callback + + def skip_filter(*names) + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.") + skip_action_callback(*names) + end # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract @@ -169,14 +187,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. @@ -185,11 +211,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/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 95c67d482b..109eff10eb 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -150,7 +150,17 @@ module AbstractController rescue LoadError => e raise AbstractController::Helpers::MissingHelperError.new(e, file_name) end - file_name.camelize.constantize + + mod_name = file_name.camelize + begin + mod_name.constantize + rescue LoadError + # dependencies.rb gives a similar error message but its wording is + # not as clear because it mentions autoloading. To the user all it + # matters is that a helper module couldn't be loaded, autoloading + # is an internal mechanism that should not leak. + raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb" + end when Module arg else @@ -174,7 +184,7 @@ module AbstractController module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path - rescue MissingSourceFile => e + rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" 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/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 9d10140ed2..5514213ad8 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -17,8 +17,8 @@ module AbstractController extend ActiveSupport::Concern include ActionView::ViewPaths - # Normalize arguments, options and then delegates render_to_body and - # sticks the result in self.response_body. + # Normalizes arguments, options and then delegates render_to_body and + # sticks the result in <tt>self.response_body</tt>. # :api: public def render(*args, &block) options = _normalize_render(*args, &block) @@ -30,11 +30,11 @@ module AbstractController # Raw rendering of a template to a string. # # It is similar to render, except that it does not - # set the response_body and it should be guaranteed + # set the +response_body+ and it should be guaranteed # to always return a string. # - # If a component extends the semantics of response_body - # (as Action Controller extends it to be anything that + # If a component extends the semantics of +response_body+ + # (as ActionController extends it to be anything that # responds to the method each), this method needs to be # overridden in order to still return a string. # :api: plugin @@ -73,8 +73,9 @@ module AbstractController } end - # Normalize args by converting render "foo" to render :action => "foo" and - # render "foo/bar" to render :file => "foo/bar". + # Normalize args by converting <tt>render "foo"</tt> to + # <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to + # <tt>render :file => "foo/bar"</tt>. # :api: plugin def _normalize_args(action=nil, options={}) if action.is_a? Hash 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 7f1aeafe8b..7667e469d3 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -11,6 +11,7 @@ module ActionController autoload :Caching autoload :Metal autoload :Middleware + autoload :Renderer autoload_under "metal" do autoload :Compatibility @@ -22,7 +23,6 @@ module ActionController autoload :ForceSSL autoload :Head autoload :Helpers - autoload :HideActions autoload :HttpAuthentication autoload :ImplicitRender autoload :Instrumentation @@ -34,7 +34,6 @@ module ActionController autoload :Rendering autoload :RequestForgeryProtection autoload :Rescue - autoload :Responder autoload :Streaming autoload :StrongParameters autoload :Testing diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 7bbf938987..e6038396f9 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -44,7 +44,7 @@ module ActionController # The full request object is available via the request accessor and is primarily used to query for HTTP headers: # # def server_ip - # location = request.env["SERVER_ADDR"] + # location = request.env["REMOTE_ADDR"] # render plain: "This server hosted at #{location}" # end # @@ -206,7 +206,6 @@ module ActionController AbstractController::AssetPaths, Helpers, - HideActions, UrlFor, Redirecting, ActionView::Layouts, diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 89fa75f025..87609d8aa7 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -1,4 +1,3 @@ - module ActionController class LogSubscriber < ActiveSupport::LogSubscriber INTERNAL_PARAMS = %w(controller action format _method only_path) @@ -54,15 +53,6 @@ module ActionController end end - def deep_munge(event) - debug do - "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ - "to nil, because it was one of [], [null] or [null, null, ...]. "\ - "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ - "for more information."\ - end - end - %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index bfbc15a901..ae111e4951 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -165,7 +165,7 @@ module ActionController headers["Location"] = url end - # basic url_for that can be overridden for more robust functionality + # Basic url_for that can be overridden for more robust functionality def url_for(string) string end @@ -173,6 +173,7 @@ module ActionController def status @_status end + alias :response_code :status # :nodoc: def status=(status) @_status = Rack::Utils.status_code(status) @@ -182,18 +183,22 @@ module ActionController body = [body] unless body.nil? || body.respond_to?(:each) super end - + # Tests if render or redirect has already happened. def performed? response_body || (response && response.committed?) 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: @@ -236,9 +241,5 @@ module ActionController lambda { |env| new.dispatch(name, klass.new(env)) } end end - - def _status_code - @_status - end end end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index a93727df90..858870d8b8 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -13,9 +13,9 @@ module ActionController end module ClassMethods - # Allows you to consider additional controller-wide information when generating an etag. + # 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 authorized displaying # of cached pages. # # class InvoicesController < ApplicationController @@ -32,7 +32,7 @@ module ActionController end end - # Sets the etag, +last_modified+, or both on the response and renders a + # Sets the +etag+, +last_modified+, or both on the response and renders a # <tt>304 Not Modified</tt> response if the request is already fresh. # # === Parameters: @@ -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 + # 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 @@ -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/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 3ca0c6837a..f9303efe6c 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -7,8 +7,8 @@ module ActionController # # config.action_controller.etag_with_template_digest = false # - # Override the template to digest by passing `:template` to `fresh_when` - # and `stale?` calls. For example: + # Override the template to digest by passing +:template+ to +fresh_when+ + # and +stale?+ calls. For example: # # # We're going to render widgets/show, not posts/show # fresh_when @post, template: 'widgets/show' diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 3844dbf2a6..18e003741d 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -25,7 +25,7 @@ module ActionController end end - class ActionController::UrlGenerationError < RoutingError #:nodoc: + class ActionController::UrlGenerationError < ActionControllerError #:nodoc: end class MethodNotAllowed < ActionControllerError #:nodoc: diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 3d2badf9c2..0d93e2f7aa 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -29,14 +29,14 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content?(self._status_code) + self.response_body = "" + + if include_content?(self.response_code) self.content_type = content_type || (Mime[formats.first] if formats) self.response.charset = false if self.response - self.response_body = " " else headers.delete('Content-Type') headers.delete('Content-Length') - self.response_body = "" end end 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 25c123edf7..20afcee537 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -53,10 +53,8 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # get( - # "/notes/1.xml", nil, - # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) - # ) + # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # get "/notes/1.xml" # # assert_equal 200, status # end @@ -108,11 +106,11 @@ 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) @@ -397,6 +395,7 @@ module ActionController # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token + TOKEN_KEY = 'token=' TOKEN_REGEX = /^Token / AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self @@ -462,16 +461,22 @@ module ActionController raw_params.map { |param| param.split %r/=(.+)?/ } end - # This removes the `"` characters wrapping the value. + # This removes the <tt>"</tt> characters wrapping the value. def rewrite_param_values(array_params) array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' } end # This method takes an authorization body and splits up the key-value - # pairs by the standardized `:`, `;`, or `\t` delimiters defined in - # `AUTHN_PAIR_DELIMITERS`. + # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt> + # delimiters defined in +AUTHN_PAIR_DELIMITERS+. def raw_params(auth) - auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + + if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}}) + _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}" + end + + _raw_params end # Encodes the given token and options into an Authorization header value. @@ -481,7 +486,7 @@ module ActionController # # Returns String. def encode_credentials(token, options = {}) - values = ["token=#{token.to_s.inspect}"] + options.map do |key, value| + values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value| "#{key}=#{value.to_s.inspect}" end "Token #{values * ", "}" diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index b0e164bc57..a3e1a71b0a 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -21,17 +21,20 @@ module ActionController :action => self.action_name, :params => request.filtered_parameters, :format => request.format.try(:ref), - :method => request.method, + :method => request.request_method, :path => (request.fullpath rescue "unknown") } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload| - result = super - payload[:status] = response.status - append_info_to_payload(payload) - result + begin + result = super + payload[:status] = response.status + result + ensure + append_info_to_payload(payload) + end end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index c9ef3a3dad..7590fb6843 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/, "\ndata: ") @stream.write "data: #{message}\n\n" end end @@ -189,12 +189,6 @@ module ActionController !@aborted end - def await_close - synchronize do - @cv.wait_until { @closed } - end - end - def on_error(&block) @error_callback = block end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index dc572f13d2..7dae171215 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,28 +1,7 @@ -require 'active_support/core_ext/array/extract_options' require 'abstract_controller/collector' module ActionController #:nodoc: module MimeResponds - extend ActiveSupport::Concern - - module ClassMethods - def respond_to(*) - raise NoMethodError, "The controller-level `respond_to' feature has " \ - "been extracted to the `responders` gem. Add it to your Gemfile to " \ - "continue using this feature:\n" \ - " gem 'responders', '~> 2.0'\n" \ - "Consult the Rails upgrade guide for details." - end - end - - def respond_with(*) - raise NoMethodError, "The `respond_with' feature has been extracted " \ - "to the `responders` gem. Add it to your Gemfile to continue using " \ - "this feature:\n" \ - " gem 'responders', '~> 2.0'\n" \ - "Consult the Rails upgrade guide for details." - end - # Without web-service support, an action which collects the data for displaying a list of people # might look something like this: # @@ -135,18 +114,6 @@ module ActionController #:nodoc: # # render json: @people # - # Since this is a common pattern, you can use the class method respond_to - # with the respond_with method to have the same results: - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end - # # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, @@ -214,13 +181,13 @@ module ActionController #:nodoc: # format.html.phone # this gets rendered # end # - # Be sure to check the documentation of +respond_with+ and - # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. - def respond_to(*mimes, &block) + # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt> + # for more examples. + def respond_to(*mimes) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? collector = Collector.new(mimes, request.variant) - block.call(collector) if block_given? + yield collector if block_given? if format = collector.negotiate_format(request) _process_format(format) @@ -234,8 +201,8 @@ module ActionController #:nodoc: # A container for responses available from the current controller for # requests for different mime-types sent to a particular action. # - # The public controller methods +respond_with+ and +respond_to+ may be called - # with a block that is used to define responses to different mime-types, e.g. + # The public controller methods +respond_to+ may be called with a block + # that is used to define responses to different mime-types, e.g. # for +respond_to+ : # # respond_to do |format| diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 2ca8955741..a7e734db42 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' -require 'active_support/core_ext/struct' require 'action_dispatch/http/mime_type' module ActionController @@ -86,7 +85,7 @@ module ActionController new name, format, include, exclude, nil, nil end - def initialize(name, format, include, exclude, klass, model) # nodoc + def initialize(name, format, include, exclude, klass, model) # :nodoc: super @include_set = include @name_set = name @@ -244,7 +243,7 @@ module ActionController request.parameters.merge! wrapped_hash request.request_parameters.merge! wrapped_hash - # This will make the wrapped hash displayed in the log file + # This will display the wrapped hash in the log file request.filtered_parameters.merge! wrapped_filtered_hash end super @@ -252,7 +251,7 @@ module ActionController private - # Returns the wrapper key which will use to stored wrapped parameters. + # Returns the wrapper key which will be used to stored 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 6921834044..ae9d89cc8c 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -6,11 +6,17 @@ module ActionController extend ActiveSupport::Concern delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :_status_code, :to => "@_response" + :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/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index bc94536c8c..45d3962494 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -86,8 +86,7 @@ module ActionController # end # end # To use renderers and their mime types in more concise ways, see - # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and - # <tt>ActionController::MimeResponds#respond_with</tt> + # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> def self.add(key, &block) define_method(_render_with_renderer_method_name(key), &block) RENDERERS << key.to_sym diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 7bbff0450a..2d15c39d88 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 diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index a4f376816f..367b736035 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,5 +1,6 @@ require 'rack/session/abstract/id' require 'action_controller/metal/exceptions' +require 'active_support/security_utils' module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -28,14 +29,7 @@ module ActionController #:nodoc: # you're building an API you'll need something like: # # class ApplicationController < ActionController::Base - # protect_from_forgery - # skip_before_action :verify_authenticity_token, if: :json_request? - # - # protected - # - # def json_request? - # request.format.json? - # end + # protect_from_forgery unless: -> { request.format.json? } # end # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, @@ -86,12 +80,18 @@ module ActionController #:nodoc: # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable CSRF protection on controller by skipping the verification before_action: + # You can disable forgery protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # # Valid Options: # - # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. + # * <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>: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: @@ -99,9 +99,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 @@ -208,6 +210,7 @@ module ActionController #:nodoc: forgery_protection_strategy.new(self).handle_unverified_request end + #:nodoc: CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ "<script> tag on another site requested protected JavaScript. " \ "If you know what you're doing, go ahead and disable forgery " \ @@ -272,7 +275,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) @@ -305,8 +310,7 @@ module ActionController #:nodoc: end def compare_with_real_token(token, session) - # Borrow a constant-time comparison from Rack - Rack::Utils.secure_compare(token, real_csrf_token(session)) + ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end def real_csrf_token(session) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7038f0997f..01bbd749c1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,5 +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' @@ -91,7 +92,11 @@ module ActionController # params.permit(:c) # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b # - # <tt>ActionController::Parameters</tt> is inherited from + # Please note that these options *are not thread-safe*. In a multi-threaded + # environment they should only be set once at boot-time and never mutated at + # runtime. + # + # <tt>ActionController::Parameters</tt> inherits from # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. # @@ -114,10 +119,12 @@ module ActionController def self.const_missing(const_name) super unless const_name == :NEVER_UNPERMITTED_PARAMS - ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ - " has been deprecated. Use "\ - "`ActionController::Parameters.always_permitted_parameters` instead." - self.always_permitted_parameters + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. + Use `ActionController::Parameters.always_permitted_parameters` instead. + MSG + + always_permitted_parameters end # Returns a new instance of <tt>ActionController::Parameters</tt>. @@ -160,6 +167,12 @@ module ActionController end end + # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + def to_unsafe_h + to_hash + end + alias_method :to_unsafe_hash, :to_unsafe_h + # Convert all hashes in values into parameters, then yield each pair like # the same way as <tt>Hash#each_pair</tt> def each_pair(&block) diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index dd8da4b5dc..d01927b7cb 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -24,7 +24,7 @@ module ActionController module ClassMethods def before_filters - _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name} + _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name) end end end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 0f2fa5fb08..572d1770f7 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -30,9 +30,9 @@ module ActionController :_recall => request.path_parameters }.merge!(super).freeze - if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || - (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) + if (same_origin = _routes.equal?(request.routes)) || + (script_name = request.engine_script_name(_routes)) || + (original_script_name = request.original_script_name) options = @_url_options.dup if original_script_name diff --git a/actionpack/lib/action_controller/model_naming.rb b/actionpack/lib/action_controller/model_naming.rb deleted file mode 100644 index 2b33f67263..0000000000 --- a/actionpack/lib/action_controller/model_naming.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ActionController - module ModelNaming - # Converts the given object to an ActiveModel compliant one. - def convert_to_model(object) - object.respond_to?(:to_model) ? object.to_model : object - end - - def model_name_from_record_or_class(record_or_class) - convert_to_model(record_or_class).model_name - end - end -end 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/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 8c10c3e7b0..4782991463 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -66,7 +66,10 @@ module ActionController def reset_template_assertion RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - instance_variable_get("@_#{instance_variable}").clear + ivar_name = "@_#{instance_variable}" + if instance_variable_defined?(ivar_name) + instance_variable_get(ivar_name).clear + end end end @@ -144,6 +147,8 @@ module ActionController 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 @@ -489,55 +494,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 @@ -561,41 +577,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!) @@ -617,6 +661,11 @@ 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 @@ -638,6 +687,13 @@ module ActionController 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 @@ -688,6 +744,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.present? + + args = args.unshift(http_method) + process(action, *args) + end + end + + REQUEST_KWARGS = %i(params session flash method body xhr) + def kwarg_request?(*args) + args[0].respond_to?(:keys) && ( + (args[0].key?(:format) && args[0].keys.size == 1) || + args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } + ) + end + + def non_kwarg_request_warning + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + ActionController::TestCase HTTP request methods will accept only + keyword arguments in future Rails versions. + + Examples: + + get :show, params: { id: 1 }, session: { user_id: 1 } + process :update, method: :post, params: { id: 1 } + MSG + end + def document_root_element html_document.root end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 11b5e6be33..dcd3ee0644 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_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/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 9c8f65deac..53a98c5d0a 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -72,11 +72,12 @@ module ActionDispatch end end end + # Sets the \variant for template. def variant=(variant) if variant.is_a?(Symbol) @variant = [variant] - elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } + elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } @variant = variant else raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index b9d5009683..7e585aa244 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -6,7 +6,7 @@ require 'active_support/core_ext/string/starts_ends_with' module Mime class Mimes < Array def symbols - @symbols ||= map { |m| m.to_sym } + @symbols ||= map(&:to_sym) end %w(<< concat shift unshift push pop []= clear compact! collect! @@ -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/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index b655a54865..df4b073a17 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -56,7 +56,7 @@ module ActionDispatch elsif value.is_a?(Array) value = value.map { |v| v.is_a?(Hash) ? call(v) : v } elsif blocks.any? - key = key.dup + key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 20ae48d458..c2f05ecc86 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/deprecation' module ActionDispatch module Http @@ -25,13 +24,6 @@ module ActionDispatch @env[PARAMETERS_KEY] = parameters end - def symbolized_path_parameters - ActiveSupport::Deprecation.warn( - "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" - ) - path_parameters - end - # Returns a hash with the \parameters used to form the \path of the request. # Returned hash keys are strings: # diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index c441f7f95b..07b3814ca4 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -50,7 +50,7 @@ module ActionDispatch @original_fullpath = nil @fullpath = nil @ip = nil - @uuid = nil + @request_id = nil end def check_path_parameters! @@ -105,6 +105,18 @@ module ActionDispatch @request_method ||= check_method(env["REQUEST_METHOD"]) end + def routes # :nodoc: + env["action_dispatch.routes".freeze] + end + + def original_script_name # :nodoc: + env['ORIGINAL_SCRIPT_NAME'.freeze] + end + + def engine_script_name(_routes) # :nodoc: + env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"] + end + def request_method=(request_method) #:nodoc: if check_method(request_method) @request_method = env["REQUEST_METHOD"] = request_method @@ -237,10 +249,12 @@ module ActionDispatch # # 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 + @request_id ||= env["action_dispatch.request_id"] end + alias_method :uuid, :request_id + # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil @@ -298,7 +312,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) - rescue TypeError, Rack::Utils::InvalidParameterError => e + rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:query, e) end alias :query_parameters :GET @@ -306,7 +320,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) - rescue TypeError, Rack::Utils::InvalidParameterError => e + rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end alias :request_parameters :POST @@ -325,15 +339,6 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility. - def deep_munge(hash) - ActiveSupport::Deprecation.warn( - "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead." - ) - - Utils.deep_munge(hash) - end - protected def parse_query(qs) Utils.deep_munge(super) @@ -341,7 +346,7 @@ module ActionDispatch private def check_method(name) - HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") name end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index c5e18048da..a895d1ab18 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/deprecation' require 'action_dispatch/http/filter_redirect' require 'monitor' @@ -114,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 @@ -283,15 +282,6 @@ module ActionDispatch # :nodoc: end alias prepare! to_a - # Be super clear that a response object is not an Array. Defining this - # would make implicit splatting work, but it also makes adding responses - # as arrays work, and "flattening" responses, cascading to the rack body! - # Not sensible behavior. - def to_ary - ActiveSupport::Deprecation.warn 'ActionDispatch::Response#to_ary no longer performs implicit conversion to an Array. Please use response.to_a instead, or a splat like `status, headers, body = *response`' - to_a - end - # Returns the response cookies, converted to a Hash of (name => value) pairs # # assert_equal 'AuthorOfNewPage', r.cookies['author'] @@ -309,9 +299,6 @@ module ActionDispatch # :nodoc: cookies end - def _status_code - @status - end private def before_committed @@ -321,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 6b8dcaf497..001b14ec97 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -12,10 +12,22 @@ module ActionDispatch self.tld_length = 1 class << self + # Returns the domain part of a host given the domain level. + # + # # Top-level domain example + # extract_domain('www.example.com', 1) # => "example.com" + # # Second-level domain example + # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk" def extract_domain(host, tld_length) extract_domain_from(host, tld_length) if named_host?(host) end + # Returns the subdomains of a host as an Array given the domain level. + # + # # Top-level domain example + # extract_subdomains('www.example.com', 1) # => ["www"] + # # Second-level domain example + # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"] def extract_subdomains(host, tld_length) if named_host?(host) extract_subdomains_from(host, tld_length) @@ -24,6 +36,12 @@ module ActionDispatch end end + # Returns the subdomains of a host as a String given the domain level. + # + # # Top-level domain example + # extract_subdomain('www.example.com', 1) # => "www" + # # Second-level domain example + # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www" def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end @@ -68,7 +86,9 @@ module ActionDispatch end def add_anchor(path, anchor) - path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param.to_s)}" + if anchor + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}" + end end def extract_domain_from(host, tld_length) @@ -171,16 +191,43 @@ module ActionDispatch end # Returns the complete URL used for this request. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.url # => "http://example.com" def url protocol + host_with_port + fullpath end # Returns 'https://' if this is an SSL request and 'http://' otherwise. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.protocol # => "http://" + # + # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on' + # req.protocol # => "https://" def protocol @protocol ||= ssl? ? 'https://' : 'http://' end # Returns the \host for this request, such as "example.com". + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.raw_host_with_port # => "example.com" + # + # 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"] forwarded.split(/,\s?/).last @@ -190,17 +237,44 @@ module ActionDispatch end # Returns the host for this request, such as example.com. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.host # => "example.com" def host raw_host_with_port.sub(/:\d+$/, '') end # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.host_with_port # => "example.com" + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.host_with_port # => "example.com:8080" def host_with_port "#{host}#{port_string}" end # Returns the port number of this request as an integer. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.port # => 80 + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.port # => 8080 def port @port ||= begin if raw_host_with_port =~ /:(\d+)$/ @@ -212,6 +286,13 @@ module ActionDispatch end # Returns the standard \port number for this request's protocol. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.standard_port # => 80 def standard_port case protocol when 'https://' then 443 @@ -220,18 +301,48 @@ module ActionDispatch end # Returns whether this request is using the standard port + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.standard_port? # => true + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.standard_port? # => false def standard_port? port == standard_port end # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.optional_port # => nil + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.optional_port # => 8080 def optional_port standard_port? ? nil : port end # Returns a string \port suffix, including colon, like ":8080" if the \port # number of this request is not the default HTTP \port 80 or HTTPS \port 443. + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.port_string # => "" + # + # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req.port_string # => ":8080" def port_string standard_port? ? '' : ":#{port}" end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 59b353b1b7..992c1a9efe 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -40,7 +40,7 @@ module ActionDispatch end message = "No route matches #{Hash[constraints.sort].inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" if name + message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? raise ActionController::UrlGenerationError, message end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index 990d2127ee..d7ce6042c2 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -88,13 +88,13 @@ module ActionDispatch erb = File.read File.join(viz_dir, 'index.html.erb') states = "function tt() { return #{to_json}; }" - fun_routes = paths.shuffle.first(3).map do |ast| + fun_routes = paths.sample(3).map do |ast| ast.map { |n| case n when Nodes::Symbol case n.left when ':id' then rand(100).to_s - when ':format' then %w{ xml json }.shuffle.first + when ':format' then %w{ xml json }.sample else 'omg' end @@ -109,7 +109,7 @@ module ActionDispatch svg = to_svg javascripts = [states, fsm_js] - # Annoying hack for 1.9 warnings + # Annoying hack warnings fun_routes = fun_routes stylesheets = stylesheets svg = svg diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index 66e414213a..0ccab21801 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -45,51 +45,6 @@ module ActionDispatch (@table.keys + @table.values.flat_map(&:keys)).uniq end - # Returns a generalized transition graph with reduced states. The states - # are reduced like a DFA, but the table must be simulated like an NFA. - # - # Edges of the GTG are regular expressions. - def generalized_table - gt = GTG::TransitionTable.new - marked = {} - state_id = Hash.new { |h,k| h[k] = h.length } - alphabet = self.alphabet - - stack = [eclosure(0)] - - until stack.empty? - state = stack.pop - next if marked[state] || state.empty? - - marked[state] = true - - alphabet.each do |alpha| - next_state = eclosure(following_states(state, alpha)) - next if next_state.empty? - - gt[state_id[state], state_id[next_state]] = alpha - stack << next_state - end - end - - final_groups = state_id.keys.find_all { |s| - s.sort.last == accepting - } - - final_groups.each do |states| - id = state_id[states] - - gt.add_accepting(id) - save = states.find { |s| - @memos.key?(s) && eclosure(s).sort.last == accepting - } - - gt.add_memo(id, memo(save)) - end - - gt - end - # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def following_states(t, a) @@ -107,7 +62,7 @@ module ActionDispatch end def alphabet - inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s } + inverted.values.flat_map(&:keys).compact.uniq.sort_by(&:to_s) end # Returns a set of NFA states reachable from some NFA state +s+ in set diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 3af940a02f..64b48ca45f 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -42,7 +42,7 @@ module ActionDispatch end def names - @names ||= spec.grep(Nodes::Symbol).map { |n| n.name } + @names ||= spec.grep(Nodes::Symbol).map(&:name) end def required_names @@ -52,7 +52,7 @@ module ActionDispatch def optional_names @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| group.grep(Nodes::Symbol) - }.map { |n| n.name }.uniq + }.map(&:name).uniq end class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: @@ -122,6 +122,11 @@ module ActionDispatch re = @matchers[node.left.to_sym] || '.+' "(#{re})" end + + def visit_OR(node) + children = node.children.map { |n| visit n } + "(?:#{children.join(?|)})" + end end class UnanchoredRegexp < AnchoredRegexp # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 9f0a3af902..4d5c18984a 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -60,7 +60,7 @@ module ActionDispatch end def parts - @parts ||= segments.map { |n| n.to_sym } + @parts ||= segments.map(&:to_sym) end alias :segment_keys :parts @@ -68,12 +68,8 @@ module ActionDispatch @path_formatter.evaluate path_options end - def optional_parts - path.optional_names.map { |n| n.to_sym } - end - def required_parts - @required_parts ||= path.required_names.map { |n| n.to_sym } + @required_parts ||= path.required_names.map(&:to_sym) end def required_default?(key) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 9131b65380..e9df984c86 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -68,8 +68,8 @@ module ActionDispatch def visualizer tt = GTG::Builder.new(ast).transition_table - groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s } - asts = groups.values.map { |v| v.first } + groups = partitioned_routes.first.map(&:ast).group_by(&:to_s) + asts = groups.values.map(&:first) tt.visualizer(asts) end @@ -121,6 +121,7 @@ module ActionDispatch end def match_head_routes(routes, req) + routes.delete_if { |route| route.verb == // } head_routes = match_routes(routes, req) if head_routes.empty? diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb index 633be11a2d..19e0bc03d6 100644 --- a/actionpack/lib/action_dispatch/journey/scanner.rb +++ b/actionpack/lib/action_dispatch/journey/scanner.rb @@ -39,18 +39,18 @@ module ActionDispatch [:SLASH, text] when text = @ss.scan(/\*\w+/) [:STAR, text] - when text = @ss.scan(/\(/) + when text = @ss.scan(/(?<!\\)\(/) [:LPAREN, text] - when text = @ss.scan(/\)/) + when text = @ss.scan(/(?<!\\)\)/) [:RPAREN, text] when text = @ss.scan(/\|/) [:OR, text] when text = @ss.scan(/\./) [:DOT, text] - when text = @ss.scan(/:\w+/) + when text = @ss.scan(/(?<!\\):\w+/) [:SYMBOL, text] - when text = @ss.scan(/[\w%\-~]+/) - [:LITERAL, text] + when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/) + [:LITERAL, text.tr('\\', '')] # any char when text = @ss.scan(/./) [:LITERAL, text] diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css index 50caebaa18..403e16a7bb 100644 --- a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css +++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css @@ -16,10 +16,6 @@ h2 { font-size: 0.5em; } -div#chart-2 { - height: 350px; -} - .clearfix {display: inline-block; } .input { overflow: show;} .instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em} diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index baf9d5779e..f80df78582 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,6 +1,6 @@ module ActionDispatch - # Provide callbacks to be executed before and after the request dispatch. + # Provides callbacks to be executed before and after dispatching the request. class Callbacks include ActiveSupport::Callbacks diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 83ac62a83d..b7687ca100 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -71,11 +71,13 @@ module ActionDispatch # restrict to the domain level. If you use a schema like www.example.com # and want to share session with user.example.com set <tt>:domain</tt> # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with - # <tt>:all</tt> again when deleting cookies. + # <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>: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. @@ -120,7 +122,7 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. @@ -143,7 +145,7 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. @@ -281,7 +283,7 @@ module ActionDispatch def handle_options(options) #:nodoc: options[:path] ||= "/" - if options[:domain] == :all + if options[:domain] == :all || options[:domain] == 'all' # if there is a provided tld length then we use it otherwise default domain regexp domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP @@ -408,7 +410,7 @@ module ActionDispatch @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end - def serialize(name, value) + def serialize(value) serializer.dump(value) end @@ -461,9 +463,9 @@ module ActionDispatch def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! - options[:value] = @verifier.generate(serialize(name, options[:value])) + options[:value] = @verifier.generate(serialize(options[:value])) else - options = { :value => @verifier.generate(serialize(name, options)) } + options = { :value => @verifier.generate(serialize(options)) } end raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @@ -479,7 +481,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and secrets.secret_key_base are both set. It reads + # 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. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: @@ -522,7 +524,7 @@ module ActionDispatch options = { :value => options } end - options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -537,7 +539,7 @@ module ActionDispatch end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base + # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base # are both set. It reads legacy cookies signed with the old dummy key generator and # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 274f6f2f22..9082aac271 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -1,6 +1,10 @@ require 'action_dispatch/http/request' require 'action_dispatch/middleware/exception_wrapper' require 'action_dispatch/routing/inspector' +require 'action_view' +require 'action_view/base' + +require 'pp' module ActionDispatch # This middleware is responsible for logging exceptions and @@ -8,6 +12,32 @@ module ActionDispatch class DebugExceptions RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__) + class DebugView < ActionView::Base + def debug_params(params) + clean_params = params.clone + clean_params.delete("action") + clean_params.delete("controller") + + if clean_params.empty? + 'None' + else + PP.pp(clean_params, "", 200) + end + end + + def debug_headers(headers) + if headers.present? + headers.inspect.gsub(',', ",\n") + else + 'None' + end + end + + def debug_hash(object) + object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + end + end + def initialize(app, routes_app = nil) @app = app @routes_app = routes_app @@ -35,12 +65,25 @@ module ActionDispatch if env['action_dispatch.show_detailed_exceptions'] request = Request.new(env) - template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], + traces = wrapper.traces + + trace_to_show = 'Application Trace' + if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' + trace_to_show = 'Full Trace' + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + template = DebugView.new([RESCUES_TEMPLATE_PATH], request: request, exception: wrapper.exception, - traces: traces_from_wrapper(wrapper), + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, routes_inspector: routes_inspector(exception), - source_extract: wrapper.source_extract, + source_extracts: wrapper.source_extracts, line_number: wrapper.line_number, file: wrapper.file ) @@ -93,36 +136,5 @@ module ActionDispatch ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) end end - - # Augment the exception traces by providing ids for all unique stack frame - def traces_from_wrapper(wrapper) - application_trace = wrapper.application_trace - framework_trace = wrapper.framework_trace - full_trace = wrapper.full_trace - - if application_trace && framework_trace - id_counter = 0 - - application_trace = application_trace.map do |trace| - prev = id_counter - id_counter += 1 - { id: prev, trace: trace } - end - - framework_trace = framework_trace.map do |trace| - prev = id_counter - id_counter += 1 - { id: prev, trace: trace } - end - - full_trace = application_trace + framework_trace - end - - { - "Application Trace" => application_trace, - "Framework Trace" => framework_trace, - "Full Trace" => full_trace - } - end end end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index b98b553c38..d176a73633 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,21 +1,23 @@ require 'action_controller/metal/exceptions' require 'active_support/core_ext/module/attribute_accessors' +require 'rack/utils' module ActionDispatch class ExceptionWrapper cattr_accessor :rescue_responses @@rescue_responses = Hash.new(:internal_server_error) @@rescue_responses.merge!( - 'ActionController::RoutingError' => :not_found, - 'AbstractController::ActionNotFound' => :not_found, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::UnknownHttpMethod' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::UnknownFormat' => :not_acceptable, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, - 'ActionDispatch::ParamsParser::ParseError' => :bad_request, - 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::UnknownHttpMethod' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, + 'ActionDispatch::ParamsParser::ParseError' => :bad_request, + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request ) cattr_accessor :rescue_templates @@ -56,24 +58,51 @@ module ActionDispatch clean_backtrace(:all) end + def traces + appplication_trace_with_ids = [] + framework_trace_with_ids = [] + full_trace_with_ids = [] + + full_trace.each_with_index do |trace, idx| + trace_with_id = { id: idx, trace: trace } + + if application_trace.include?(trace) + appplication_trace_with_ids << trace_with_id + else + framework_trace_with_ids << trace_with_id + end + + full_trace_with_ids << trace_with_id + end + + { + "Application Trace" => appplication_trace_with_ids, + "Framework Trace" => framework_trace_with_ids, + "Full Trace" => full_trace_with_ids + } + end + def self.status_code_for_exception(class_name) Rack::Utils.status_code(@@rescue_responses[class_name]) end - def source_extract - exception.backtrace.map do |trace| - file, line = trace.split(":") - line_number = line.to_i + def source_extracts + backtrace.map do |trace| + file, line_number = extract_file_and_line_number(trace) + { code: source_fragment(file, line_number), - file: file, line_number: line_number } - end if exception.backtrace + end end private + def backtrace + Array(@exception.backtrace) + end + def original_exception(exception) if registered_original_exception?(exception) exception.original_exception @@ -88,9 +117,9 @@ module ActionDispatch def clean_backtrace(*args) if backtrace_cleaner - backtrace_cleaner.clean(@exception.backtrace, *args) + backtrace_cleaner.clean(backtrace, *args) else - @exception.backtrace + backtrace end end @@ -110,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 e90f8b9ce6..59639a010e 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -79,22 +79,31 @@ module ActionDispatch class FlashHash include Enumerable - def self.from_session_value(value) - 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) + def self.from_session_value(value) #:nodoc: + 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 - def to_session_value - return nil if empty? - {'discard' => @discard.to_a, 'flashes' => @flashes} + # 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: + flashes_to_keep = @flashes.except(*@discard) + return nil if flashes_to_keep.empty? + {'flashes' => flashes_to_keep} end def initialize(flashes = {}, discard = []) #:nodoc: @@ -132,7 +141,7 @@ module ActionDispatch end def key?(name) - @flashes.key? name + @flashes.key? name.to_s end def delete(key) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index b426183488..29d43faeed 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -47,7 +47,7 @@ module ActionDispatch else false end - rescue Exception => e # JSON or Ruby code block errors + rescue => e # JSON or Ruby code block errors logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" raise ParseError.new(e.message, e) diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 25658bac3d..b9ca524309 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,23 @@ 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: + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: + HTTP_X_REQUEST_ID = "HTTP_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"] } + 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] } end private def external_request_id(env) - if request_id = env["HTTP_X_REQUEST_ID"].presence - request_id.gsub(/[^\w\-]/, "").first(255) + if request_id = env[HTTP_X_REQUEST_ID].presence + request_id.gsub(/[^\w\-]/, "".freeze).first(255) end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index e66c21ef85..2e1bd45c3d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -23,10 +23,19 @@ module ActionDispatch 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}"] - if match = paths.detect {|p| File.file?(File.join(@root, p)) } + if match = paths.detect { |p| + path = File.join(@root, p) + begin + File.file?(path) && File.readable?(path) + rescue SystemCallError + false + end + + } return ::Rack::Utils.escape(match) end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb index db219c8fa9..49b1e83551 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb @@ -5,20 +5,8 @@ <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre> <% end %> -<% - clean_params = @request.filtered_parameters.clone - clean_params.delete("action") - clean_params.delete("controller") - - request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") - - def debug_hash(object) - object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") - end unless self.class.method_defined?(:debug_hash) -%> - <h2 style="margin-top: 30px">Request</h2> -<p><b>Parameters</b>:</p> <pre><%= request_dump %></pre> +<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre> <div class="details"> <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div> @@ -31,4 +19,4 @@ </div> <h2 style="margin-top: 30px">Response</h2> -<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre> +<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb index 51660a619b..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb @@ -1,29 +1,27 @@ -<% if @source_extract %> - <% @source_extract.each_with_index do |extract_source, index| %> - <% if extract_source[:code] %> - <div class="source <%="hidden" if index != 0%>" id="frame-source-<%=index%>"> - <div class="info"> - Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>): - </div> - <div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% extract_source[:code].keys.each do |line_number| %> +<% @source_extracts.each_with_index do |source_extract, index| %> + <% if source_extract[:code] %> + <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>"> + <div class="info"> + Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>): + </div> + <div class="data"> + <table cellpadding="0" cellspacing="0" class="lines"> + <tr> + <td> + <pre class="line_numbers"> + <% source_extract[:code].each_key do |line_number| %> <span><%= line_number -%></span> - <% end %> - </pre> - </td> + <% end %> + </pre> + </td> <td width="100%"> <pre> -<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[:line_number] -%>"><%= source -%></div><% end -%> +<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%> </pre> </td> - </tr> - </table> - </div> + </tr> + </table> </div> - <% end %> + </div> <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index f62caf51d7..ab57b11c7d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -12,7 +12,7 @@ <% end %> <% @traces.each do |name, trace| %> - <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> + <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;"> <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre> </div> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb index 5c016e544e..2a65fd06ad 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb @@ -4,4 +4,8 @@ <div id="container"> <h2><%= h @exception.message %></h2> + + <%= render template: "rescues/_source" %> + <%= render template: "rescues/_trace" %> + <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb index 7e9cedb95e..55dd5ddc7b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb @@ -27,4 +27,6 @@ <%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %> <% end %> + + <%= render template: "rescues/_request_and_response" %> </div> 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 6ffa242da4..5cee0b5932 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -1,6 +1,6 @@ <% content_for :style do %> #route_table { - margin: 0 auto 0; + margin: 0; border-collapse: collapse; } diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 9d4f1aa3c5..1c9371d89c 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -16,10 +16,6 @@ module ActionDispatch when Array v.grep(Hash) { |x| deep_munge(x, keys) } v.compact! - if v.empty? - hash[k] = nil - ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys) - end when Hash deep_munge(v, keys) end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index ea3b2f419d..df5ebb6751 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -48,7 +48,7 @@ module ActionDispatch def reqs @reqs ||= begin reqs = endpoint - reqs += " #{constraints.to_s}" unless constraints.empty? + reqs += " #{constraints}" unless constraints.empty? reqs end end @@ -114,9 +114,7 @@ module ActionDispatch def collect_routes(routes) routes.collect do |route| RouteWrapper.new(route) - end.reject do |route| - route.internal? - end.collect do |route| + end.reject(&:internal?).collect do |route| collect_engine_routes(route) { name: route.name, diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index fc28740828..34b5b48f3a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -7,7 +7,6 @@ require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' -require 'active_support/deprecation' module ActionDispatch module Routing @@ -243,12 +242,10 @@ module ActionDispatch def app(blocks) if to.respond_to?(:call) Constraints.new(to, blocks, false) + elsif blocks.any? + Constraints.new(dispatcher(defaults), blocks, true) else - if blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) - else - dispatcher(defaults) - end + dispatcher(defaults) end end @@ -280,14 +277,8 @@ module ActionDispatch end def split_to(to) - case to - when Symbol - ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" - [nil, to.to_s] - when /#/ then to.split('#') - when String - ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" - [to, nil] + if to =~ /#/ + to.split('#') else [] end @@ -382,7 +373,7 @@ module ActionDispatch # Matches a url pattern to one or more routes. # - # You should not use the `match` method in your router + # You should not use the +match+ method in your router # without specifying an HTTP method. # # If you want to expose your action to both GET and POST, use: @@ -393,7 +384,7 @@ module ActionDispatch # Note that +:controller+, +:action+ and +:id+ are interpreted as url # query parameters and thus available through +params+ in an action. # - # If you want to expose your action to GET, use `get` in the router: + # If you want to expose your action to GET, use +get+ in the router: # # Instead of: # @@ -434,7 +425,7 @@ module ActionDispatch # # Because requesting various HTTP verbs with a single action has security # implications, you must either specify the actions in - # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] + # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers] # instead +match+ # # === Options @@ -448,7 +439,7 @@ module ActionDispatch # The route's action. # # [:param] - # Overrides the default resource identifier `:id` (name of the + # Overrides the default resource identifier +:id+ (name of the # dynamic segment used to generate the routes). # You can access that segment from your controller using # <tt>params[<:param>]</tt>. @@ -573,13 +564,7 @@ module ActionDispatch raise "A rack application must be specified" unless path rails_app = rails_app? app - - if rails_app - options[:as] ||= app.railtie_name - else - # non rails apps can't have an :as - options[:as] = nil - end + options[:as] ||= app_name(app, rails_app) target_as = name_for_action(options[:as], path) options[:via] ||= :all @@ -611,6 +596,15 @@ module ActionDispatch app.is_a?(Class) && app < Rails::Railtie end + def app_name(app, rails_app) + if rails_app + app.railtie_name + elsif app.is_a?(Class) + class_name = app.name + ActiveSupport::Inflector.underscore(class_name).tr("/", "_") + end + end + def define_generate_prefix(app, name) _route = @set.named_routes.get name _routes = @set @@ -1510,7 +1504,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: @@ -1745,9 +1739,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. diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index f15868d37e..2e116ea9cd 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -1,5 +1,3 @@ -require 'action_controller/model_naming' - module ActionDispatch module Routing # Polymorphic URL helpers are methods for smart resolution to a named route call when @@ -55,8 +53,6 @@ module ActionDispatch # form_for([blog, @post]) # => "/blog/posts/1" # module PolymorphicRoutes - include ActionController::ModelNaming - # Constructs a call to a named RESTful route for the given record and returns the # resulting URL string. For example: # @@ -116,7 +112,6 @@ module ActionDispatch action, type, opts - end # Returns the path component of a URL for the given record. It uses @@ -159,8 +154,7 @@ module ActionDispatch end def polymorphic_path_for_action(action, record_or_hash, options) - options = options.merge(:action => action, :routing_type => :path) - polymorphic_path(record_or_hash, options) + polymorphic_path(record_or_hash, options.merge(:action => action)) end class HelperMethodBuilder # :nodoc: @@ -253,7 +247,7 @@ module ActionDispatch args = [] model = record.to_model - name = if record.persisted? + name = if model.persisted? args << model model.model_name.singular_route_key else @@ -296,11 +290,12 @@ module ActionDispatch when Class @key_strategy.call record.model_name else - if record.persisted? - args << record.to_model - record.to_model.model_name.singular_route_key + model = record.to_model + if model.persisted? + args << model + model.model_name.singular_route_key else - @key_strategy.call record.to_model.model_name + @key_strategy.call model.model_name end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index f51bee3b31..ce04f0b08a 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,9 +85,9 @@ 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 + attr_reader :routes, :url_helpers_module, :path_helpers_module def initialize @routes = {} @@ -101,11 +102,6 @@ module ActionDispatch @path_helpers.include?(key) || @url_helpers.include?(key) end - def helpers - ActiveSupport::Deprecation.warn("`named_routes.helpers` is deprecated, please use `route_defined?(route_name)` to see if a named route was defined.") - @path_helpers + @url_helpers - end - def helper_names @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s) end @@ -135,7 +131,7 @@ module ActionDispatch end routes[key] = route define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH - define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL + define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN @path_helpers << path_name @url_helpers << url_name @@ -166,26 +162,7 @@ module ActionDispatch routes.length end - def path_helpers_module(warn = false) - if warn - mod = @path_helpers_module - helpers = @path_helpers - Module.new do - include mod - - helpers.each do |meth| - define_method(meth) do |*args, &block| - ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead") - super(*args, &block) - end - end - end - else - @path_helpers_module - end - 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) @@ -200,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) @@ -276,15 +253,22 @@ module ActionDispatch end def handle_positional_args(controller_options, inner_options, args, result, path_params) - if args.size > 0 - if args.size < path_params.size - 1 # take format into account + # take format into account + if path_params.include?(:format) + path_params_size = path_params.size - 1 + else + path_params_size = path_params.size + end + + if args.size < path_params_size path_params -= controller_options.keys path_params -= result.keys end - path_params.each { |param| - result[param] = inner_options[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) @@ -317,12 +301,9 @@ module ActionDispatch end end - # :stopdoc: # strategy for building urls to send to the client PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } - FULL = ->(options) { ActionDispatch::Http::URL.full_url_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 @@ -397,7 +378,7 @@ module ActionDispatch Routing::RouteSet::Dispatcher.new(defaults) end - module MountedHelpers #:nodoc: + module MountedHelpers extend ActiveSupport::Concern include UrlFor end @@ -414,9 +395,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 @@ -427,7 +410,7 @@ module ActionDispatch RUBY end - def url_helpers(include_path_helpers = true) + def url_helpers(supports_path = true) routes = self Module.new do @@ -438,7 +421,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 @@ -454,14 +444,12 @@ module ActionDispatch # named routes... include url_helpers - if include_path_helpers + if supports_path path_helpers = routes.named_routes.path_helpers_module - else - path_helpers = routes.named_routes.path_helpers_module(true) - end - include path_helpers - extend path_helpers + include path_helpers + extend path_helpers + end # plus a singleton class method called _routes ... included do @@ -472,6 +460,12 @@ module ActionDispatch # UrlFor (included in this module) add extra # conveniences for working with @_routes. define_method(:_routes) { @_routes || routes } + + define_method(:_generate_paths_by_default) do + supports_path + end + + private :_generate_paths_by_default end end @@ -493,7 +487,7 @@ module ActionDispatch path = conditions.delete :path_info ast = conditions.delete :parsed_path_info path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) + conditions = build_conditions(conditions, path.names.map(&:to_sym)) route = @set.add_route(app, path, conditions, defaults, name) named_routes[name] = route if name @@ -550,12 +544,12 @@ module ActionDispatch end private :build_conditions - class Generator #:nodoc: + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value elsif value.is_a?(Array) - value.map { |v| v.to_param }.join('/') + value.map(&:to_param).join('/') elsif param = value.to_param param end @@ -699,10 +693,10 @@ module ActionDispatch end def find_script_name(options) - options.delete(:script_name) { '' } + options.delete(:script_name) || '' end - def path_for(options, route_name = nil) # :nodoc: + def path_for(options, route_name = nil) url_for(options, route_name, PATH) end @@ -788,5 +782,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 eb554ec383..dca86858cc 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -184,6 +184,12 @@ 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/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb deleted file mode 100644 index fb579b52fe..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.")
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index e06f7037c6..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,13 +150,7 @@ module ActionDispatch old_controller, @controller = @controller, @controller.clone _routes = @routes - # Unfortunately, there is currently an abstraction leak between AC::Base - # and AV::Base which requires having the URL helpers in both AC and AV. - # To do this safely at runtime for tests, we need to bump up the helper serial - # to that the old AV subclass isn't cached. - # - # TODO: Make this unnecessary - @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/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb deleted file mode 100644 index 7361e6c44b..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been extracted to the rails-dom-testing gem.") diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb deleted file mode 100644 index 5c2905d1ac..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::TagAssertions has been extracted to the rails-dom-testing gem.") diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2d1c3ac5c7..2fe37c5bd4 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -12,12 +12,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 +30,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 +75,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 +114,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 +222,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 +289,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.present? + 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 +330,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 +345,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,23 +386,51 @@ module ActionDispatch module Runner include ActionDispatch::Assertions - def app - @app ||= nil + APP_SESSIONS = {} + + attr_reader :app + + def before_setup + super + @app = nil + @integration_session = nil + 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: + @integration_session = nil end %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_template_assertion - # reset the html_document variable, but only for new get/post calls - @html_document = nil unless method == 'cookies' || method == 'assigns' + # 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 copy_session_variables! end @@ -358,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 @@ -380,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! @@ -389,11 +483,6 @@ module ActionDispatch super end end - - private - def integration_session - @integration_session ||= nil - end end end @@ -472,6 +561,84 @@ module ActionDispatch # end # end # end + # + # Another longer example would be: + # + # A simple integration test that exercises multiple controllers: + # + # require 'test_helper' + # + # class UserFlowsTest < ActionDispatch::IntegrationTest + # test "login and browse site" do + # # login via https + # https! + # get "/login" + # assert_response :success + # + # post_via_redirect "/login", username: users(:david).username, password: users(:david).password + # assert_equal '/welcome', path + # assert_equal 'Welcome david!', flash[:notice] + # + # https!(false) + # get "/articles/all" + # assert_response :success + # assert assigns(:articles) + # end + # end + # + # As you can see the integration test involves multiple controllers and + # exercises the entire stack from database to dispatcher. In addition you can + # have multiple session instances open simultaneously in a test and extend + # those instances with assertion methods to create a very powerful testing + # DSL (domain-specific language) just for your application. + # + # Here's an example of multiple sessions and custom DSL in an integration test + # + # require 'test_helper' + # + # class UserFlowsTest < ActionDispatch::IntegrationTest + # test "login and browse site" do + # # User david logs in + # david = login(:david) + # # User guest logs in + # guest = login(:guest) + # + # # Both are now available in different sessions + # assert_equal 'Welcome david!', david.flash[:notice] + # assert_equal 'Welcome guest!', guest.flash[:notice] + # + # # User david can browse site + # david.browses_site + # # User guest can browse site as well + # guest.browses_site + # + # # Continue with other assertions + # end + # + # private + # + # module CustomDsl + # def browses_site + # get "/products/all" + # assert_response :success + # assert assigns(:products) + # end + # end + # + # def login(user) + # open_session do |sess| + # sess.extend(CustomDsl) + # u = users(user) + # sess.https! + # sess.post "/login", username: u.username, password: u.password + # assert_equal '/welcome', sess.path + # sess.https!(false) + # end + # end + # end + # + # Consult the Rails Testing Guide for more. + class IntegrationTest < ActiveSupport::TestCase include Integration::Runner include ActionController::TemplateAssertions @@ -492,7 +659,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_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index de3dc5f924..4b9a088265 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -60,7 +60,7 @@ module ActionDispatch def accept=(mime_types) @env.delete('action_dispatch.request.accepts') - @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",") + @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") end alias :rack_cookies :cookies 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/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 77f656d6f1..f664dab620 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 280d35adcb..255ac9f4ed 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -5,10 +5,10 @@ module ActionPack end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end 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 69312e4c22..918589f916 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -21,6 +21,7 @@ PROCESS_COUNT = (ENV['N'] || 4).to_i require 'active_support/testing/autorun' require 'abstract_controller' +require 'abstract_controller/railties/routes_helpers' require 'action_controller' require 'action_view' require 'action_view/testing/resolvers' @@ -53,23 +54,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 {|locale| locale.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 @@ -128,22 +114,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 @@ -194,6 +164,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase yield temporary_routes ensure self.class.app = old_app + self.remove! silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } end @@ -259,7 +230,7 @@ end module ActionController class Base # This stub emulates the Railtie including the URL helpers from a Rails application - include SharedTestRoutes.url_helpers + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) include SharedTestRoutes.mounted_helpers self.view_paths = FIXTURE_LOAD_PATH diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index f2a4503f13..21586e2193 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -378,7 +378,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 @@ -575,6 +577,13 @@ class AssertTemplateTest < ActionController::TestCase 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" diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 950788743e..f7ad8e5158 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 @@ -35,10 +15,6 @@ class NonEmptyController < ActionController::Base def public_action render :nothing => true end - - hide_action :hidden_action - def hidden_action - end end class DefaultUrlOptionsController < ActionController::Base @@ -51,6 +27,16 @@ class DefaultUrlOptionsController < ActionController::Base end end +class OptionalDefaultUrlOptionsController < ActionController::Base + def show + render nothing: true + 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..2d6607041d 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,60 @@ class ViewCacheDependencyTest < ActionController::TestCase assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies end end + +class CollectionCacheController < ActionController::Base + 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.cache_store = ActiveSupport::Cache::MemoryStore.new + end + + def test_collection_fetches_cached_views + get :index + + ActionView::PartialRenderer.expects(:collection_with_template).never + get :index + 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 + + ActionView::PartialRenderer.expects(:collection_with_template).never + get :index_with_comment + 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 b2b01b3fa9..26b94f0db8 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -10,7 +10,7 @@ class ActionController::Base def before_actions filters = _process_action_callbacks.select { |c| c.kind == :before } - filters.map! { |c| c.raw_filter } + filters.map!(&:raw_filter) end end @@ -26,7 +26,6 @@ class ActionController::Base end class FilterTest < ActionController::TestCase - class TestController < ActionController::Base before_action :ensure_login after_action :clean_up @@ -225,6 +224,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 @@ -504,7 +527,6 @@ class FilterTest < ActionController::TestCase def non_yielding_action @filters << "it didn't yield" - @filter_return_value end def action_three @@ -528,32 +550,15 @@ class FilterTest < ActionController::TestCase end end - def test_non_yielding_around_actions_not_returning_false_do_not_raise + def test_non_yielding_around_actions_do_not_raise controller = NonYieldingAroundFilterController.new - controller.instance_variable_set "@filter_return_value", true assert_nothing_raised do test_process(controller, "index") end end - def test_non_yielding_around_actions_returning_false_do_not_raise - controller = NonYieldingAroundFilterController.new - controller.instance_variable_set "@filter_return_value", false - assert_nothing_raised do - test_process(controller, "index") - end - end - - def test_after_actions_are_not_run_if_around_action_returns_false - controller = NonYieldingAroundFilterController.new - controller.instance_variable_set "@filter_return_value", false - test_process(controller, "index") - assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] - end - def test_after_actions_are_not_run_if_around_action_does_not_yield controller = NonYieldingAroundFilterController.new - controller.instance_variable_set "@filter_return_value", true test_process(controller, "index") assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] end @@ -614,6 +619,16 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login ), assigns["ran_filter"] end + def test_if_is_ignored_when_used_with_only + test_process(SkipFilterUsingOnlyAndIf, 'login') + assert_nil assigns['ran_filter'] + end + + def test_except_is_ignored_when_used_with_if + test_process(SkipFilterUsingIfAndExcept, 'login') + assert_equal %w(ensure_login), assigns["ran_filter"] + end + def test_skipping_class_actions test_process(ClassController) assert_equal true, assigns["ran_class_action"] @@ -951,8 +966,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 @@ -1021,24 +1043,37 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_first_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_1') - assert_equal ' ', response.body + assert_equal '', response.body assert_equal 1, controller.instance_variable_get(:@try) end def test_second_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_2') - assert_equal ' ', response.body + assert_equal '', response.body assert_equal 2, controller.instance_variable_get(:@try) end def test_last_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_3') - assert_equal ' ', response.body + assert_equal '', response.body 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)', assigns['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 + 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 50b36a0567..081288ef21 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -29,6 +29,15 @@ module ActionDispatch assert_equal 'world', @hash['hello'] end + def test_key + @hash['foo'] = 'bar' + + assert @hash.key?('foo') + assert @hash.key?(:foo) + assert_not @hash.key?('bar') + assert_not @hash.key?(:bar) + end + def test_delete @hash['foo'] = 'bar' @hash.delete 'foo' @@ -48,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..0ff0a1ef61 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -288,16 +288,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 +312,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..04222745d9 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 @@ -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 diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 20f99f19ee..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 @@ -60,6 +60,12 @@ class HelpersPathsController < ActionController::Base end end +class HelpersTypoController < ActionController::Base + path = File.expand_path('../../fixtures/helpers_typo', __FILE__) + $:.unshift(path) + self.helpers_path = path +end + module LocalAbcHelper def a() end def b() end @@ -82,6 +88,22 @@ class HelperPathsTest < ActiveSupport::TestCase end end +class HelpersTypoControllerTest < ActiveSupport::TestCase + def setup + @autoload_paths = ActiveSupport::Dependencies.autoload_paths + ActiveSupport::Dependencies.autoload_paths = Array(HelpersTypoController.helpers_path) + end + + def test_helper_typo_error_message + e = assert_raise(NameError) { HelpersTypoController.helper 'admin/users' } + assert_equal "Couldn't find Admin::UsersHelper, expected it to be defined in helpers/admin/users_helper.rb", e.message + end + + def teardown + ActiveSupport::Dependencies.autoload_paths = @autoload_paths + end +end + class HelperTest < ActiveSupport::TestCase class TestController < ActionController::Base attr_accessor :delegate_attr @@ -201,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..20962a90cb 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -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 diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 8c6c8a0aa7..a758df2ec6 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -162,17 +162,36 @@ class HttpTokenAuthenticationTest < ActionController::TestCase assert_equal(expected, actual) end + test "token_and_options returns right token when token key is not specified in header" do + token = "rcHu+HzSFw89Ypyhn/896A=" + + actual = ActionController::HttpAuthentication::Token.token_and_options( + sample_request_without_token_key(token) + ).first + + expected = token + assert_equal(expected, actual) + end + private def sample_request(token, options = {nonce: "def"}) authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)| arr << "#{k}=\"#{v}\"" end.join(", ") - @sample_request ||= OpenStruct.new authorization: authorization + mock_authorization_request(authorization) end def malformed_request - @malformed_request ||= OpenStruct.new authorization: %{Token token=} + mock_authorization_request(%{Token token=}) + end + + def sample_request_without_token_key(token) + mock_authorization_request(%{Token #{token}}) + end + + def mock_authorization_request(authorization) + OpenStruct.new(authorization: authorization) end def encode_credentials(token, options = {}) diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 99b53e6fd2..438c044da2 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 @@ -279,6 +396,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 @@ -383,7 +505,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 +529,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 +552,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 +640,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 +675,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 +719,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"] @@ -615,6 +777,8 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest get 'bar', :to => 'application_integration_test/test#index', :as => :bar mount MountedApp => '/mounted', :as => "mounted" + get 'fooz' => proc { |env| [ 200, {'X-Cascade' => 'pass'}, [ "omg" ] ] }, :anchor => false + get 'fooz', :to => 'application_integration_test/test#index' end def app @@ -631,6 +795,12 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest assert_equal '/mounted/baz', mounted.baz_path end + test "path after cascade pass" do + get '/fooz' + assert_equal 'index', response.body + assert_equal '/fooz', path + end + test "route helpers after controller access" do get '/' assert_equal '/', empty_string_path @@ -654,7 +824,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 @@ -684,7 +854,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'] @@ -806,3 +976,63 @@ class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest assert_response :ok end end + +class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def index + render plain: 'ok' + end + end + + def test_with_routing_resets_session + klass_namespace = self.class.name.underscore + + with_routing do |routes| + routes.draw do + namespace klass_namespace do + resources :foo, path: '/with' + end + end + + get '/integration_with_routing_test/with' + assert_response 200 + assert_equal 'ok', response.body + end + + with_routing do |routes| + routes.draw do + namespace klass_namespace do + resources :foo, path: '/routing' + end + end + + get '/integration_with_routing_test/routing' + assert_response 200 + assert_equal 'ok', response.body + end + end +end + +# to work in contexts like rspec before(:all) +class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest + self._setup_callbacks = [] + self._teardown_callbacks = [] + + class FooController < ActionController::Base + def ok + cookies[:key] = 'ok' + render plain: 'ok' + end + end + + def test_request + with_routing do |routes| + routes.draw { get ':action' => FooController } + get '/ok' + + assert_response 200 + assert_equal 'ok', response.body + assert_equal 'ok', cookies['key'] + end + end +end diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index 27871ef351..3576015513 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -19,7 +19,7 @@ class LocalizedTemplatesTest < ActionController::TestCase def test_localized_template_is_used I18n.locale = :de get :hello_world - assert_equal "Gutten Tag", @response.body + assert_equal "Guten Tag", @response.body end def test_default_locale_template_is_used_when_locale_is_missing @@ -30,11 +30,11 @@ 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 - assert_equal "Gutten Tag", @response.body + assert_equal "Guten Tag", @response.body end def test_localized_template_has_correct_header_with_no_format_in_template_name diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 49be7caf38..03a4ad7823 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -73,6 +73,16 @@ module Another def with_action_not_found raise AbstractController::ActionNotFound end + + def append_info_to_payload(payload) + super + payload[:test_key] = "test_value" + @last_payload = payload + end + + def last_payload + @last_payload + end end end @@ -130,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 @@ -138,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 @@ -150,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 @@ -163,10 +173,22 @@ class ACLogSubscriberTest < ActionController::TestCase assert_match(/\(Views: [\d.]+ms\)/, logs[1]) end + def test_append_info_to_payload_is_called_even_with_exception + begin + get :with_exception + wait + rescue Exception + end + + assert_equal "test_value", @controller.last_payload[:test_key] + end + 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 1bc7ad3015..1f5f66dc80 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -310,17 +310,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 +335,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 +357,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 +408,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 +474,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 +497,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 +525,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 +545,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 +559,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 @@ -579,17 +579,17 @@ class RespondToControllerTest < ActionController::TestCase end get :using_defaults - assert_equal "using_defaults - #{[:html].to_s}", @response.body + assert_equal "using_defaults - #{[:html]}", @response.body - get :using_defaults, :format => "xml" - assert_equal "using_defaults - #{[:xml].to_s}", @response.body + get :using_defaults, format: "xml" + assert_equal "using_defaults - #{[:xml]}", @response.body end def test_format_with_custom_response_type 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,7 +603,7 @@ 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 diff --git a/actionpack/test/controller/mime/responders_test.rb b/actionpack/test/controller/mime/responders_test.rb deleted file mode 100644 index 032b4c0ab1..0000000000 --- a/actionpack/test/controller/mime/responders_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'abstract_unit' -require 'controller/fake_models' - -class ResponderTest < ActionController::TestCase - def test_class_level_respond_to - e = assert_raises(NoMethodError) do - Class.new(ActionController::Base) do - respond_to :json - end - end - - assert_includes e.message, '`responders` gem' - assert_includes e.message, '~> 2.0' - end - - def test_respond_with - klass = Class.new(ActionController::Base) do - def index - respond_with Customer.new("david", 13) - end - end - - @controller = klass.new - - e = assert_raises(NoMethodError) do - get :index - end - - assert_includes e.message, '`responders` gem' - assert_includes e.message, '~> 2.0' - end -end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 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_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb index a961cbf849..0c21bb0719 100644 --- a/actionpack/test/controller/new_base/render_file_test.rb +++ b/actionpack/test/controller/new_base/render_file_test.rb @@ -13,15 +13,6 @@ module RenderFile render :file => File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar') end - def without_file_key - render File.join(File.dirname(__FILE__), *%w[.. .. fixtures test hello_world]) - end - - def without_file_key_with_instance_variable - @secret = 'in the sauce' - render File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar') - end - def relative_path @secret = 'in the sauce' render :file => '../../fixtures/test/render_file_with_ivar' @@ -41,11 +32,6 @@ module RenderFile path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals') render :file => path, :locals => {:secret => 'in the sauce'} end - - def without_file_key_with_locals - path = FIXTURES.join('test/render_file_with_locals').to_s - render path, :locals => {:secret => 'in the sauce'} - end end class TestBasic < Rack::TestCase @@ -61,16 +47,6 @@ module RenderFile assert_response "The secret is in the sauce\n" end - test "rendering path without specifying the :file key" do - get :without_file_key - assert_response "Hello world!" - end - - test "rendering path without specifying the :file key with ivar" do - get :without_file_key_with_instance_variable - assert_response "The secret is in the sauce\n" - end - test "rendering a relative path" do get :relative_path assert_response "The secret is in the sauce\n" @@ -90,10 +66,5 @@ module RenderFile get :with_locals assert_response "The secret is in the sauce\n" end - - test "rendering path without specifying the :file key with locals" do - get :without_file_key_with_locals - assert_response "The secret is in the sauce\n" - 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 e87811776a..19fef718e7 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -45,6 +45,10 @@ module RenderTemplate render :template => "locals", :locals => { :secret => 'area51' } end + def with_locals_without_key + render "locals", :locals => { :secret => 'area51' } + end + def builder_template render :template => "xml_template" end @@ -101,8 +105,13 @@ module RenderTemplate assert_response "The secret is area51" end + test "rendering a template with local variables without key" do + get :with_locals + assert_response "The secret is area51" + 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 @@ -117,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 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/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index ba98ad7605..2ed486516d 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -280,4 +280,10 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal({ "controller" => "users", "action" => "create" }, params.to_h) end + + test "to_unsafe_h returns unfiltered params" do + assert @params.to_h.is_a? Hash + assert_not @params.to_h.is_a? ActionController::Parameters + assert_equal @params.to_hash, @params.to_unsafe_h + end end diff --git a/actionpack/test/controller/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/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 b036b6c08e..488585c7a4 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -58,24 +58,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 @@ -129,50 +129,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 +173,15 @@ class TestController < ActionController::Base head :forbidden, :x_custom_header => "something" end + def head_with_no_content + # Fill in the headers with dummy data to make + # sure they get removed during the testing + response.headers["Content-Type"] = "dummy" + response.headers["Content-Length"] = 42 + + head 204 + end + private def set_variable_for_layout @@ -344,7 +309,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'] @@ -355,6 +319,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 @@ -373,6 +338,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'] @@ -518,21 +511,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 @@ -540,13 +533,21 @@ 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 + def test_head_with_no_content + get :head_with_no_content + + assert_equal 204, @response.status + assert_nil @response.headers["Content-Type"] + assert_nil @response.headers["Content-Length"] + 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 @@ -560,3 +561,56 @@ class HeadRenderTest < ActionController::TestCase assert_response :forbidden 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_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 3e0bfe8d14..8887f291cf 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -103,6 +103,31 @@ class RequestForgeryProtectionControllerUsingNullSession < ActionController::Bas 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 + class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession self.allow_forgery_protection = false @@ -237,23 +262,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 +340,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 +366,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 +463,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 +550,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 +561,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 a5f43c4b6b..f3da2df3ef 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -43,11 +43,11 @@ class ResourcesTest < ActionController::TestCase :member => member_methods, :path_names => path_names do |options| - collection_methods.keys.each do |action| + collection_methods.each_key do |action| assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", :action => action end - member_methods.keys.each do |action| + member_methods.each_key do |action| assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", :action => action, :id => "1" end @@ -150,7 +150,7 @@ class ResourcesTest < ActionController::TestCase end assert_restful_named_routes_for :messages do |options| - actions.keys.each do |action| + actions.each_key do |action| assert_named_route "/messages/#{action}", "#{action}_messages_path", :action => action end end @@ -180,7 +180,7 @@ class ResourcesTest < ActionController::TestCase end assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| - actions.keys.each do |action| + actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action end end @@ -207,7 +207,7 @@ class ResourcesTest < ActionController::TestCase end assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| - actions.keys.each do |action| + actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action end end @@ -237,7 +237,7 @@ class ResourcesTest < ActionController::TestCase end assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| - actions.keys.each do |action| + actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", :action => action, :format => 'xml' end end @@ -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 c18914cc8e..2d08987ca6 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' @@ -884,13 +883,13 @@ class RouteSetTest < ActiveSupport::TestCase set.draw { get ':controller/(:action(/:id))' } path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal "/foo/bar/15", path - assert_equal %w(that this), extras.map { |e| e.to_s }.sort + assert_equal %w(that this), extras.map(&:to_s).sort end def test_extra_keys set.draw { get ':controller/:action/:id' } extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") - assert_equal %w(that this), extras.map { |e| e.to_s }.sort + assert_equal %w(that this), extras.map(&:to_s).sort end def test_generate_extras_not_first @@ -900,7 +899,7 @@ class RouteSetTest < ActiveSupport::TestCase end path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal "/foo/bar/15", path - assert_equal %w(that this), extras.map { |e| e.to_s }.sort + assert_equal %w(that this), extras.map(&:to_s).sort end def test_generate_not_first @@ -918,7 +917,7 @@ class RouteSetTest < ActiveSupport::TestCase get ':controller/:action/:id' end extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") - assert_equal %w(that this), extras.map { |e| e.to_s }.sort + assert_equal %w(that this), extras.map(&:to_s).sort end def test_draw @@ -1001,6 +1000,9 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal "http://test.host/people?baz=bar#location", controller.send(:index_url, :baz => "bar", :anchor => 'location') + + assert_equal "http://test.host/people", controller.send(:index_url, anchor: nil) + assert_equal "http://test.host/people", controller.send(:index_url, anchor: false) end def test_named_route_url_method_with_port @@ -1383,7 +1385,7 @@ class RouteSetTest < ActiveSupport::TestCase url = controller.url_for({ :controller => "connection", :only_path => true }) assert_equal "/connection/connection", url - url = controller.url_for({ :use_route => :family_connection, + url = controller.url_for({ :use_route => "family_connection", :controller => "connection", :only_path => true }) assert_equal "/connection", url end 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..fba5ebba15 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,7 +72,7 @@ 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) @@ -80,7 +80,7 @@ module ShowExceptions 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) @@ -88,7 +88,7 @@ module ShowExceptions 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 1280e0d9a3..e348749f78 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -1,61 +1,67 @@ require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/json/decoding' +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_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 @@ -63,7 +69,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> @@ -85,7 +91,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> @@ -94,15 +100,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 @@ -110,32 +116,40 @@ 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 + render nothing: true end def test_assigns @foo = "foo" - @foo_hash = {:foo => :bar} - render :nothing => true + @foo_hash = { foo: :bar } + render nothing: true + end + + def test_without_body + render html: '<div class="foo"></div>'.html_safe + end + + def test_with_body + render html: '<body class="foo"></body>'.html_safe end private def generate_url(opts) - url_for(opts.merge(:action => "test_uri")) + url_for(opts.merge(action: "test_uri")) end end @@ -155,7 +169,7 @@ XML class ViewAssignsController < ActionController::Base def test_assigns @foo = "foo" - render :nothing => true + render nothing: true end def view_assigns @@ -179,6 +193,19 @@ XML end end + def test_assert_select_without_body + get :test_without_body + + assert_select 'body', 0 + assert_select 'div.foo' + end + + def test_assert_select_with_body + get :test_with_body + + assert_select 'body.foo' + end + def test_url_options_reset @controller = DefaultUrlOptionsCachingController.new get :test_url_options_reset @@ -187,32 +214,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 @@ -221,25 +266,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" @@ -249,22 +311,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] @@ -272,7 +360,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 @@ -299,19 +387,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 @@ -323,9 +432,9 @@ 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 @@ -340,7 +449,7 @@ XML assert_equal "foo", assigns["foo"] # but the assigned variable should not have its own keys stringified - expected_hash = { :foo => :bar } + expected_hash = { foo: :bar } assert_equal expected_hash, assigns(:foo_hash) end @@ -368,21 +477,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 @@ -394,29 +503,75 @@ 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_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', @@ -426,7 +581,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', @@ -436,7 +591,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', @@ -447,7 +612,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( @@ -458,8 +625,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 @@ -482,28 +649,35 @@ 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.keys.each do |key| + @request.path_parameters.each_key do |key| assert_kind_of Symbol, key end end @@ -532,19 +706,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 @@ -552,15 +764,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 @@ -581,19 +793,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 + + def test_request_format_kwarg + get :test_format, format: 'html' assert_equal 'text/html', @response.body - get :test_format, :format => 'json' + 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 @@ -678,7 +909,10 @@ XML 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 @@ -694,10 +928,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 @@ -720,6 +965,90 @@ 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 + + routes.draw do + get '/' => 'bar#index' + end + end + + class BarController < ActionController::Base + def index + render text: 'bar' + end + end + + class BarControllerTest < ActionController::TestCase + tests BarController + + def test_engine_controller_route + get :index + assert_equal @response.body, 'bar' + end + end + + class BarControllerTestWithExplicitRouteSet < ActionController::TestCase + tests BarController + + def setup + @routes = Engine.routes + end + + def test_engine_controller_route + get :index + assert_equal @response.body, 'bar' + end + end +end + class InferringClassNameTest < ActionController::TestCase def test_determine_controller_class assert_equal ContentController, determine_class("ContentControllerTest") @@ -770,7 +1099,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 @@ -779,7 +1108,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 @@ -800,29 +1129,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 969129d9ba..0ffa2d2a03 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -25,8 +25,7 @@ module AbstractController path = klass.new.fun_path({:controller => :articles, :baz => "baz", - :zot => "zot", - :only_path => true }) + :zot => "zot"}) # :bar key isn't provided assert_equal '/foo/zot', path end @@ -55,6 +54,20 @@ module AbstractController ) end + def test_nil_anchor + assert_equal( + '/c/a', + W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: nil) + ) + end + + def test_false_anchor + assert_equal( + '/c/a', + W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: false) + ) + end + def test_anchor_should_call_to_param assert_equal('/c/a#anchor', W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor')) @@ -277,6 +290,13 @@ module AbstractController end end + def test_using_nil_script_name_properly_concats_with_original_script_name + add_host! + assert_equal('https://www.basecamphq.com/subdir/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => nil, :original_script_name => '/subdir') + ) + end + def test_only_path with_routing do |set| set.draw do @@ -291,7 +311,7 @@ module AbstractController assert_equal '/brave/new/world', controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true) - assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused', :only_path => true)) + assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused')) assert_equal("/home/sweet/home/alabama", controller.home_path('alabama')) end end diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index d80b0e2da0..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,19 +75,33 @@ 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 end + def test_parsing_json_doesnot_rescue_exception + with_test_route_set do + with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do + assert_raises(Interrupt) do + post "/", + params: '{"title":"JSON"}}', + headers: { 'CONTENT_TYPE' => 'application/json' } + end + end + end + end + private def with_params_parsers(parsers = {}) old_session = @integration_session diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 7dc6c37522..6223a52a76 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -1,12 +1,5 @@ require 'abstract_unit' - -begin - require 'openssl' - OpenSSL::PKCS5 -rescue LoadError, NameError - $stderr.puts "Skipping KeyGenerator test: broken OpenSSL install" -else - +require 'openssl' require 'active_support/key_generator' require 'active_support/message_verifier' @@ -152,11 +145,21 @@ class CookiesTest < ActionController::TestCase head :ok end + def set_cookie_with_domain_all_as_string + cookies[:user_name] = {:value => "rizwanreza", :domain => 'all'} + head :ok + end + def delete_cookie_with_domain cookies.delete(:user_name, :domain => :all) head :ok end + def delete_cookie_with_domain_all_as_string + cookies.delete(:user_name, :domain => 'all') + head :ok + end + def set_cookie_with_domain_and_tld cookies[:user_name] = {:value => "rizwanreza", :domain => :all, :tld_length => 2} head :ok @@ -494,7 +497,7 @@ class CookiesTest < ActionController::TestCase assert_nil @response.cookies["user_id"] end - def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature + def test_accessing_nonexistent_signed_cookie_should_not_raise_an_invalid_signature get :set_signed_cookie assert_nil @controller.send(:cookies).signed[:non_existant_attribute] end @@ -612,7 +615,7 @@ class CookiesTest < ActionController::TestCase assert_nil @response.cookies["foo"] end - def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message + def test_accessing_nonexistent_encrypted_cookie_should_not_raise_invalid_message get :set_encrypted_cookie assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute] end @@ -984,6 +987,13 @@ class CookiesTest < ActionController::TestCase assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" end + def test_cookie_with_all_domain_option_using_a_non_standard_2_letter_tld + @request.host = "admin.lvh.me" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.lvh.me; path=/" + end + def test_cookie_with_all_domain_option_using_host_with_port_and_tld_length @request.host = "nextangle.local:3000" get :set_cookie_with_domain_and_tld @@ -1155,5 +1165,3 @@ class CookiesTest < ActionController::TestCase end end end - -end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 0add9fa3b0..7921f05688 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -19,6 +19,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest @closed = true end + def method_that_raises + raise StandardError.new 'error in framework' + end + def call(env) env['action_dispatch.show_detailed_exceptions'] = @detailed req = ActionDispatch::Request.new(env) @@ -39,6 +43,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest raise ActionController::InvalidAuthenticityToken when "/not_found_original_exception" raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new) + when "/missing_template" + raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer') when "/bad_request" raise ActionController::BadRequest when "/missing_keys" @@ -57,7 +63,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest {}) raise ActionView::Template::Error.new(template, e) end - + when "/framework_raises" + method_that_raises else raise "puke!" end @@ -79,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 @@ -101,44 +108,53 @@ 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 assert_no_match '<|>', routing_table, "there should not be escaped html in the output" end + test 'displays request and response info when a RoutingError occurs' do + @app = DevelopmentApp + + get "/pass", headers: { 'action_dispatch.show_exceptions' => true } + + assert_select 'h2', /Request/ + assert_select 'h2', /Response/ + end + 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 @@ -147,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 @@ -188,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 @@ -197,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 @@ -205,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) @@ -213,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', @@ -225,24 +241,47 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/RuntimeError\n\s+in FeaturedTileController/, body) end + test "show formatted params" do + @app = DevelopmentApp + + params = { + 'id' => 'unknown', + 'someparam' => { + 'foo' => 'bar', + 'abc' => 'goo' + } + } + + get("/runtime_error", headers: { + 'action_dispatch.show_exceptions' => true, + 'action_dispatch.request.parameters' => { + 'action' => 'show', + 'controller' => 'featured_tile' + }.merge(params) + }) + assert_response 500 + + assert_includes(body, CGI.escapeHTML(PP.pp(params, "", 200))) + end + 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 @@ -255,14 +294,14 @@ 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 @@ -270,14 +309,65 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end + test 'display backtrace on template missing errors' do + @app = DevelopmentApp + + get "/missing_template" + + assert_select "header h1", /Template is missing/ + + assert_select "#container h2", /^Missing template/ + + assert_select '#Application-Trace' + assert_select '#Framework-Trace' + assert_select '#Full-Trace' + + assert_select 'h2', /Request/ + end + 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/ end end + + test 'debug exceptions app shows user code that caused the error in source view' do + @app = DevelopmentApp + Rails.stubs(:root).returns(Pathname.new('.')) + cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| + bc.add_silencer { |line| line =~ /method_that_raises/ } + bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } + end + + get '/framework_raises', headers: { 'action_dispatch.backtrace_cleaner' => cleaner } + + # Assert correct error + assert_response 500 + assert_select 'h2', /error in framework/ + + # assert source view line is the call to method_that_raises + assert_select 'div.source:not(.hidden)' do + assert_select 'pre .line.active', /method_that_raises/ + end + + # assert first source view (hidden) that throws the error + assert_select 'div.source:first' do + assert_select 'pre .line.active', /raise StandardError\.new/ + end + + # assert application trace refers to line that calls method_that_raises is first + assert_select '#Application-Trace' do + assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} + end + + # assert framework trace that that threw the error is first + assert_select '#Framework-Trace' do + assert_select 'pre code a:first', /method_that_raises/ + end + end end diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb new file mode 100644 index 0000000000..7a29a7ff97 --- /dev/null +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -0,0 +1,114 @@ +require 'abstract_unit' + +module ActionDispatch + class ExceptionWrapperTest < ActionDispatch::IntegrationTest + class TestError < StandardError + attr_reader :backtrace + + def initialize(*backtrace) + @backtrace = backtrace.flatten + end + end + + class BadlyDefinedError < StandardError + def backtrace + nil + end + end + + setup do + Rails.stubs(:root).returns(Pathname.new('.')) + + cleaner = ActiveSupport::BacktraceCleaner.new + cleaner.add_silencer { |line| line !~ /^lib/ } + + @environment = { 'action_dispatch.backtrace_cleaner' => cleaner } + end + + test '#source_extracts fetches source fragments for every backtrace entry' do + exception = TestError.new("lib/file.rb:42:in `index'") + wrapper = ExceptionWrapper.new({}, exception) + + wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo') + + 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'")) + wrapper = ExceptionWrapper.new(@environment, exception) + + assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace + end + + test '#application_trace cannot be nil' do + nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.application_trace + assert_equal [], nil_cleaner_wrapper.application_trace + end + + test '#framework_trace returns traces outside the application' do + exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) + wrapper = ExceptionWrapper.new(@environment, exception) + + assert_equal caller, wrapper.framework_trace + end + + test '#framework_trace cannot be nil' do + nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.framework_trace + assert_equal [], nil_cleaner_wrapper.framework_trace + end + + test '#full_trace returns application and framework traces' do + exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) + wrapper = ExceptionWrapper.new(@environment, exception) + + assert_equal exception.backtrace, wrapper.full_trace + end + + test '#full_trace cannot be nil' do + nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.full_trace + assert_equal [], nil_cleaner_wrapper.full_trace + end + + test '#traces returns every trace by category enumerated with an index' do + exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'") + wrapper = ExceptionWrapper.new(@environment, exception) + + assert_equal({ + 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ], + 'Framework Trace' => [ id: 1, trace: "/gems/rack.rb:43:in `index'" ], + 'Full Trace' => [ + { id: 0, trace: "lib/file.rb:42:in `index'" }, + { id: 1, trace: "/gems/rack.rb:43:in `index'" } + ] + }, wrapper.traces) + end + end +end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 7f340ced41..3017a9c2d6 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' class MimeTypeTest < ActiveSupport::TestCase test "parse single" do - Mime::LOOKUP.keys.each do |mime_type| + Mime::LOOKUP.each_key do |mime_type| unless mime_type == 'image/*' assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) end @@ -83,7 +83,7 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse broken acceptlines" do accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5" expect = [Mime::HTML, Mime::XML, "image/*", Mime::TEXT, Mime::ALL] - assert_equal expect, Mime::Type.parse(accept).collect { |c| c.to_s } + assert_equal expect, Mime::Type.parse(accept).collect(&:to_s) end # Accept header send with user HTTP_USER_AGENT: Mozilla/4.0 @@ -91,7 +91,7 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse other broken acceptlines" do accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL] - assert_equal expect, Mime::Type.parse(accept).collect { |c| c.to_s } + assert_equal expect, Mime::Type.parse(accept).collect(&:to_s) end test "custom type" do 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 c609075e6b..d77341bc64 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -39,7 +39,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest test "nils are stripped from collections" do assert_parses( - {"person" => nil}, + {"person" => []}, "{\"person\":[null]}", { 'CONTENT_TYPE' => 'application/json' } ) assert_parses( @@ -47,7 +47,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest "{\"person\":[\"foo\",null]}", { 'CONTENT_TYPE' => 'application/json' } ) assert_parses( - {"person" => nil}, + {"person" => []}, "{\"person\":[null, null]}", { 'CONTENT_TYPE' => 'application/json' } ) end @@ -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 4e99c26e03..bc6716525e 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -95,8 +95,8 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => nil}, "action") assert_parses({"action" => {"foo" => nil}}, "action[foo]") assert_parses({"action" => {"foo" => { "bar" => nil }}}, "action[foo][bar]") - assert_parses({"action" => {"foo" => { "bar" => nil }}}, "action[foo][bar][]") - assert_parses({"action" => {"foo" => nil }}, "action[foo][]") + assert_parses({"action" => {"foo" => { "bar" => [] }}}, "action[foo][bar][]") + assert_parses({"action" => {"foo" => [] }}, "action[foo][]") assert_parses({"action"=>{"foo"=>[{"bar"=>nil}]}}, "action[foo][][bar]") end @@ -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 00b80c7357..ee8e915610 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -683,6 +683,22 @@ class RequestMethod < BaseRequestTest end end + test "exception on invalid HTTP method unaffected by I18n settings" do + old_locales = I18n.available_locales + old_enforce = I18n.config.enforce_available_locales + + begin + I18n.available_locales = [:nl] + I18n.config.enforce_available_locales = true + assert_raise(ActionController::UnknownHttpMethod) do + stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method + end + ensure + I18n.available_locales = old_locales + I18n.config.enforce_available_locales = old_enforce + end + end + test "post masquerading as patch" do request = stub_request( 'REQUEST_METHOD' => 'PATCH', @@ -915,7 +931,7 @@ class RequestParameters < BaseRequestTest 2.times do assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string request.parameters end end @@ -941,7 +957,7 @@ class RequestParameters < BaseRequestTest ) assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string request.parameters end end @@ -950,7 +966,7 @@ class RequestParameters < BaseRequestTest request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") e = assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string request.parameters end @@ -981,8 +997,8 @@ class RequestParameterFilter < BaseRequestTest } parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - before_filter['barg'] = {'bargain'=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}} - after_filter['barg'] = {'bargain'=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}} + before_filter['barg'] = {:bargain=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}} + after_filter['barg'] = {:bargain=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}} assert_equal after_filter, parameter_filter.filter(before_filter) end @@ -1127,6 +1143,13 @@ class RequestVariant < BaseRequestTest end end + test "reset variant" do + request = stub_request + + request.variant = nil + assert_equal nil, request.variant + end + test "setting variant with non symbol value" do request = stub_request assert_raise ArgumentError do diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 48342e252a..5fbd19acdf 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -231,9 +231,9 @@ class ResponseTest < ActiveSupport::TestCase assert_equal ['Not Found'], body.each.to_a end - test "[response].flatten does not recurse infinitely" do + test "[response.to_a].flatten does not recurse infinitely" do Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely - status, headers, body = assert_deprecated { [@response].flatten } + status, headers, body = [@response.to_a].flatten assert_equal @response.status, status assert_equal @response.headers, headers assert_equal @response.body, body.each.to_a.join @@ -251,27 +251,9 @@ class ResponseTest < ActiveSupport::TestCase status, headers, body = Rack::ContentLength.new(app).call(env) assert_equal '5', headers['Content-Length'] end - - test "implicit destructuring and Array conversion is deprecated" do - response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) - - assert_deprecated do - status, headers, body = response - - assert_equal 404, status - assert_equal({ 'Content-Type' => 'text/plain' }, headers) - assert_equal ['Not Found'], body.each.to_a - end - - assert_deprecated { response.to_ary } - end 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 ff33dd5652..3d3d4b74ae 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -2,6 +2,11 @@ require 'abstract_unit' require 'rails/engine' require 'action_dispatch/routing/inspector' +class MountedRackApp + def self.call(env) + end +end + module ActionDispatch module Routing class RoutesInspectorTest < ActiveSupport::TestCase @@ -204,19 +209,36 @@ module ActionDispatch ], output end - class RackApp - def self.call(env) + def test_rake_routes_shows_route_with_rack_app + output = draw do + get 'foo/:id' => MountedRackApp, :id => /[A-Z]\d{5}/ end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /foo/:id(.:format) MountedRackApp {:id=>/[A-Z]\\d{5}/}" + ], output end - def test_rake_routes_shows_route_with_rack_app + def test_rake_routes_shows_named_route_with_mounted_rack_app output = draw do - get 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/ + mount MountedRackApp => '/foo' end assert_equal [ - "Prefix Verb URI Pattern Controller#Action", - " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}" + " Prefix Verb URI Pattern Controller#Action", + "mounted_rack_app /foo MountedRackApp" + ], output + end + + def test_rake_routes_shows_overridden_named_route_with_mounted_rack_app_with_name + output = draw do + mount MountedRackApp => '/foo', as: 'blog' + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " blog /foo MountedRackApp" ], output end @@ -229,21 +251,21 @@ module ActionDispatch output = draw do scope :constraint => constraint.new do - mount RackApp => '/foo' + mount MountedRackApp => '/foo' end end assert_equal [ - "Prefix Verb URI Pattern Controller#Action", - " /foo #{RackApp.name} {:constraint=>( my custom constraint )}" + " Prefix Verb URI Pattern Controller#Action", + "mounted_rack_app /foo MountedRackApp {:constraint=>( my custom constraint )}" ], output end def test_rake_routes_dont_show_app_mounted_in_assets_prefix output = draw do - get '/sprockets' => RackApp + get '/sprockets' => MountedRackApp end - assert_no_match(/RackApp/, output.first) + assert_no_match(/MountedRackApp/, output.first) assert_no_match(/\/sprockets/, output.first) end diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb index c465d56bde..fe52c50336 100644 --- a/actionpack/test/dispatch/routing/route_set_test.rb +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -69,6 +69,42 @@ module ActionDispatch end end + test "only_path: true with *_url and no :host option" do + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_equal '/foo', url_helpers.foo_url(only_path: true) + end + + test "only_path: false with *_url and no :host option" do + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_raises ArgumentError do + assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false) + end + end + + test "only_path: false with *_url and local :host option" do + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false, host: 'example.com') + end + + test "only_path: false with *_url and global :host option" do + @set.default_url_options = { host: 'example.com' } + + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false) + end + test "explicit keys win over implicit keys" do draw do resources :foo do @@ -80,6 +116,18 @@ module ActionDispatch assert_equal '/foo/1/bar/2', url_helpers.foo_bar_path(2, foo_id: 1) end + test "having an optional scope with resources" do + draw do + scope "(/:foo)" do + resources :users + end + end + + assert_equal '/users/1', url_helpers.user_path(1) + assert_equal '/users/1', url_helpers.user_path(1, foo: nil) + assert_equal '/a/users/1', url_helpers.user_path(1, foo: 'a') + end + private def draw(&block) @set.draw(&block) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d141210ad9..d65d2e8e8f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -362,22 +362,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 @@ -1430,6 +1430,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 +1692,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 @@ -3331,30 +3340,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'comments#index', @response.body end - def test_mix_symbol_to_controller_action - assert_deprecated do - draw do - get '/projects', controller: 'project_files', - action: 'index', - to: :show - end - end - get '/projects' - assert_equal 'project_files#show', @response.body - end - - def test_mix_string_to_controller_action_no_hash - assert_deprecated do - draw do - get '/projects', controller: 'project_files', - action: 'index', - to: 'show' - end - end - get '/projects' - assert_equal 'show#index', @response.body - end - def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes draw do scope shallow_path: 'projects', shallow_prefix: 'project' do @@ -3463,6 +3448,62 @@ 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 + + # HEAD request matches `get /home` rather than the lower-precedence + # Rack app mounted at `/` + head '/home' + assert_response :success + assert_equal 'test#index', @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) @@ -3559,12 +3600,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 @@ -3629,15 +3670,13 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest assert_match(/Missing :controller/, ex.message) end - def test_missing_action + def test_missing_controller_with_to ex = assert_raises(ArgumentError) { - assert_deprecated do - draw do - get '/foo/bar', :to => 'foo' - end + draw do + get '/foo/bar', :to => 'foo' end } - assert_match(/Missing :action/, ex.message) + assert_match(/Missing :controller/, ex.message) end def test_missing_action_on_hash @@ -3761,7 +3800,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 @@ -4420,7 +4459,7 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest include Routes.url_helpers - test "url helpers raise a helpful error message whem generation fails" do + test "url helpers raise a helpful error message when generation fails" do url, missing = { action: 'show', controller: 'products', id: nil }, [:id] message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" @@ -4433,3 +4472,31 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest assert_equal message, error.message end end + +class TestDefaultUrlOptions < ActionDispatch::IntegrationTest + class PostsController < ActionController::Base + def archive + render :text => "posts#archive" + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + default_url_options locale: 'en' + scope ':locale', format: false do + get '/posts/:year/:month/:day', to: 'posts#archive', as: 'archived_posts' + end + end + + APP = build_app Routes + + def app + APP + end + + include Routes.url_helpers + + def test_positional_args_with_format_false + assert_equal '/en/posts/2014/12/13', archived_posts_path(2014, 12, 13) + end +end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index c5cd24d06e..2194efa503 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -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..fbd82945cc 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -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..72eaa916bc 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -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 6f7373201c..ebc9d71403 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -1,6 +1,4 @@ -# encoding: utf-8 require 'abstract_unit' -require 'rbconfig' require 'zlib' module StaticTests @@ -200,7 +198,8 @@ class StaticTest < ActiveSupport::TestCase } def setup - @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60") + @root = "#{FIXTURE_LOAD_PATH}/public" + @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end def public_path @@ -208,11 +207,28 @@ class StaticTest < ActiveSupport::TestCase end include StaticTests + + def test_custom_handler_called_when_file_is_outside_root + filename = 'shared.html.erb' + assert File.exist?(File.join(@root, '..', filename)) + env = { + "REQUEST_METHOD"=>"GET", + "REQUEST_PATH"=>"/..%2F#{filename}", + "PATH_INFO"=>"/..%2F#{filename}", + "REQUEST_URI"=>"/..%2F#{filename}", + "HTTP_VERSION"=>"HTTP/1.1", + "SERVER_NAME"=>"localhost", + "SERVER_PORT"=>"8080", + "QUERY_STRING"=>"" + } + assert_equal(DummyApp.call(nil), @app.call(env)) + end end class StaticEncodingTest < StaticTest def setup - @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, max-age=60") + @root = "#{FIXTURE_LOAD_PATH}/公共" + @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end def public_path diff --git a/actionpack/test/dispatch/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb index 3c393f937b..7278754b49 100644 --- a/actionpack/test/dispatch/template_assertions_test.rb +++ b/actionpack/test/dispatch/template_assertions_test.rb @@ -10,7 +10,7 @@ class AssertTemplateController < ActionController::Base end def render_with_layout - @variable_for_layout = nil + @variable_for_layout = 'hello' render 'test/hello_world', layout: "layouts/standard" end @@ -95,4 +95,16 @@ class AssertTemplateControllerTest < ActionDispatch::IntegrationTest session.assert_template file: nil end end + + def test_assigns_do_not_reset_template_assertion + get '/assert_template/render_with_layout' + assert_equal 'hello', assigns(:variable_for_layout) + assert_template layout: 'layouts/standard' + end + + def test_cookies_do_not_reset_template_assertion + get '/assert_template/render_with_layout' + cookies + assert_template layout: 'layouts/standard' + end end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index 65ad8677f3..cc35d4594e 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -18,7 +18,7 @@ class TestRequestTest < ActiveSupport::TestCase assert_equal "0.0.0.0", env.delete("REMOTE_ADDR") assert_equal "Rails Testing", env.delete("HTTP_USER_AGENT") - assert_equal [1, 2], env.delete("rack.version") + assert_equal [1, 3], env.delete("rack.version") assert_equal "", env.delete("rack.input").string assert_kind_of StringIO, env.delete("rack.errors") assert_equal true, env.delete("rack.multithread") 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..d5f6e3b491 --- /dev/null +++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb @@ -0,0 +1,4 @@ +<%# I'm a comment %> +<% cache customer do %> + <%= 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..67e9f6d411 --- /dev/null +++ b/actionpack/test/fixtures/customers/_customer.html.erb @@ -0,0 +1,3 @@ +<% cache customer do %> + <%= customer.name %>, <%= customer.id %> +<% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb new file mode 100644 index 0000000000..7d2326e04d --- /dev/null +++ b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb @@ -0,0 +1,5 @@ +module Admin + module UsersHelpeR + end +end + diff --git a/actionpack/test/fixtures/localized/hello_world.de.html b/actionpack/test/fixtures/localized/hello_world.de.html index 4727d7a7e0..a8fc612c60 100644 --- a/actionpack/test/fixtures/localized/hello_world.de.html +++ b/actionpack/test/fixtures/localized/hello_world.de.html @@ -1 +1 @@ -Gutten Tag
\ No newline at end of file +Guten Tag
\ 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/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index 9dfdfc23ed..6939b426f6 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -16,6 +16,7 @@ module ActionDispatch '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?\Z}, '/:controller/*foo' => %r{\A/(#{x})/(.+)\Z}, '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z}, + '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))\Z}, }.each do |path, expected| define_method(:"test_to_regexp_#{path}") do strexp = Router::Strexp.build( @@ -39,6 +40,7 @@ module ActionDispatch '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?}, '/:controller/*foo' => %r{\A/(#{x})/(.+)}, '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar}, + '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))}, }.each do |path, expected| define_method(:"test_to_non_anchored_regexp_#{path}") do strexp = Router::Strexp.build( diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb index 624e6df51a..7a510f1e07 100644 --- a/actionpack/test/journey/route/definition/scanner_test.rb +++ b/actionpack/test/journey/route/definition/scanner_test.rb @@ -11,12 +11,25 @@ module ActionDispatch # /page/:id(/:action)(.:format) def test_tokens [ - ['/', [[:SLASH, '/']]], - ['*omg', [[:STAR, '*omg']]], - ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]], - ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]], - ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]], - ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]], + ['/', [[:SLASH, '/']]], + ['*omg', [[:STAR, '*omg']]], + ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]], + ['/page!', [[:SLASH, '/'], [:LITERAL, 'page!']]], + ['/page$', [[:SLASH, '/'], [:LITERAL, 'page$']]], + ['/page&', [[:SLASH, '/'], [:LITERAL, 'page&']]], + ["/page'", [[:SLASH, '/'], [:LITERAL, "page'"]]], + ['/page*', [[:SLASH, '/'], [:LITERAL, 'page*']]], + ['/page+', [[:SLASH, '/'], [:LITERAL, 'page+']]], + ['/page,', [[:SLASH, '/'], [:LITERAL, 'page,']]], + ['/page;', [[:SLASH, '/'], [:LITERAL, 'page;']]], + ['/page=', [[:SLASH, '/'], [:LITERAL, 'page=']]], + ['/page@', [[:SLASH, '/'], [:LITERAL, 'page@']]], + ['/page\:', [[:SLASH, '/'], [:LITERAL, 'page:']]], + ['/page\(', [[:SLASH, '/'], [:LITERAL, 'page(']]], + ['/page\)', [[:SLASH, '/'], [:LITERAL, 'page)']]], + ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]], + ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]], + ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]], ['/(:page)', [ [:SLASH, '/'], [:LPAREN, '('], 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 8e90d4100f..19c61b5914 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -201,6 +201,16 @@ module ActionDispatch assert_match(/missing required keys: \[:id\]/, error.message) end + def test_does_not_include_missing_keys_message + route_name = "gorby_thunderhorse" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate(route_name, { }, { }) + end + + assert_no_match(/missing required keys: \[\]/, error.message) + end + def test_X_Cascade add_routes @router, [ "/messages(.:format)" ] resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) @@ -405,7 +415,7 @@ module ActionDispatch def test_generate_with_name path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, "tasks" path, params = @formatter.generate( "tasks", diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb index 09ca7ff73b..0028aaa629 100644 --- a/actionpack/test/routing/helper_test.rb +++ b/actionpack/test/routing/helper_test.rb @@ -26,20 +26,6 @@ module ActionDispatch x.new.pond_duck_path Duck.new end end - - def test_path_deprecation - rs = ::ActionDispatch::Routing::RouteSet.new - rs.draw do - resources :ducks - end - - x = Class.new { - include rs.url_helpers(false) - } - assert_deprecated do - assert_equal '/ducks', x.new.ducks_path - end - end end end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 5e0b134ba4..86e20ad202 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,184 +1,61 @@ -* Add support for ARIA attributes in tags. +* Fixed a dependency tracker bug that caused template dependencies not + count layouts as dependencies for partials. - Example: + *Juho Leinonen* - <%= f.text_field :name, aria: { required: "true", hidden: "false" } %> - - now generates: - - <input aria-hidden="false" aria-required="true" id="user_name" name="user[name]" type="text"> - - *Paola Garcia Casadiego* - -* Provide a `builder` object when using the `label` form helper in block form. - - The new `builder` object responds to `translation`, allowing I18n fallback support - when you want to customize how a particular label is presented. - - *Alex Robbin* - -* Add I18n support for input/textarea placeholder text. - - Placeholder I18n follows the same convention as `label` I18n. - - *Alex Robbin* - -* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree. - - *DHH* - -* Add `PartialIteration` object used when rendering collections. - - The iteration object is available as the local variable - `#{template_name}_iteration` when rendering partials with collections. - - It gives access to the `size` of the collection being iterated over, - the current `index` and two convenience methods `first?` and `last?`. - - *Joel Junström*, *Lucas Uyezu* - -* Return an absolute instead of relative path from an asset url in the case - of the `asset_host` proc returning nil. - - *Jolyon Pawlyn* - -* Fix `html_escape_once` to properly handle hex escape sequences (e.g. ᨫ). - - *John F. Douthat* - -* Added String support for min and max properties for date field helpers. +* Extracted `ActionView::Helpers::RecordTagHelper` to external gem + (`record_tag_helper`) and added removal notices. *Todd Bealmear* -* The `highlight` helper now accepts a block to be used instead of the `highlighter` - option. - - *Lucas Mazza* - -* The `except` and `highlight` helpers now accept regular expressions. - - *Jan Szumiec* - -* Flatten the array parameter in `safe_join`, so it behaves consistently with - `Array#join`. - - *Paul Grayson* - -* Honor `html_safe` on array elements in tag values, as we do for plain string - values. - - *Paul Grayson* - -* Add `ActionView::Template::Handler.unregister_template_handler`. - - It performs the opposite of `ActionView::Template::Handler.register_template_handler`. - - *Zuhao Wan* - -* Bring `cache_digest` rake tasks up-to-date with the latest API changes. - - *Jiri Pospisil* - -* Allow custom `:host` option to be passed to `asset_url` helper that - overwrites `config.action_controller.asset_host` for particular asset. - - *Hubert Łępicki* - -* Deprecate `AbstractController::Base.parent_prefixes`. - Override `AbstractController::Base.local_prefixes` when you want to change - where to find views. - - *Nick Sutterer* - -* Take label values into account when doing I18n lookups for model attributes. - - The following: - - # form.html.erb - <%= form_for @post do |f| %> - <%= f.label :type, value: "long" %> - <% end %> - - # en.yml - en: - activerecord: - attributes: - post/long: "Long-form Post" - - Used to simply return "long", but now it will return "Long-form - Post". - - *Joshua Cody* - -* Change `asset_path` to use File.join to create proper paths: - - Before: - - https://some.host.com//assets/some.js - - After: - - https://some.host.com/assets/some.js - - *Peter Schröder* - -* Change `favicon_link_tag` default mimetype from `image/vnd.microsoft.icon` to - `image/x-icon`. - - Before: - - #=> favicon_link_tag 'myicon.ico' - <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> - - After: - - #=> favicon_link_tag 'myicon.ico' - <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" /> +* Allow to pass a string value to `size` option in `image_tag` and `video_tag`. - *Geoffroy Lorieux* + This makes the behavior more consistent with `width` or `height` options. -* Remove wrapping div with inline styles for hidden form fields. + *Mehdi Lahmam* - We are dropping HTML 4.01 and XHTML strict compliance since input tags directly - inside a form are valid HTML5, and the absence of inline styles help in validating - for Content Security Policy. +* Partial template name does no more have to be a valid Ruby identifier. - *Joost Baaij* + 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. -* `collection_check_boxes` respects `:index` option for the hidden filed name. + *Akira Matsuda* - Fixes #14147. +* Change the default template handler from `ERB` to `Raw`. - *Vasiliy Ermolovich* + Files without a template handler in their extension will be rendered using the raw + handler instead of ERB. -* `date_select` helper with option `with_css_classes: true` does not overwrite other classes. + *Rafael Mendonça França* - *Izumi Wong-Horiuchi* +* Remove deprecated `AbstractController::Base::parent_prefixes`. -* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY` - as input. + *Rafael Mendonça França* - Fixes #14405. +* Default translations that have a lower precedence than a html safe default, + but are not themselves safe, should not be marked as html_safe. - *Yves Senn* + *Justin Coyne* -* Add `include_hidden` option to `collection_check_boxes` helper. +* Make possible to use blocks with short version of `render "partial"` helper. - *Vasiliy Ermolovich* + *Nikolay Shebanov* -* Fixed a problem where the default options for the `button_tag` helper is not - applied correctly. +* Add a `hidden_field` on the `file_field` to avoid raise a error when the only + input on the form is the `file_field`. - Fixes #14254. + *Mauro George* - *Sergey Prikhodko* +* Add an explicit error message, in `ActionView::PartialRenderer` for partial + `rendering`, when the value of option `as` has invalid characters. -* Take variants into account when calculating template digests in ActionView::Digestor. + *Angelo Capilleri* - The arguments to ActionView::Digestor#digest are now being passed as a hash - to support variants and allow more flexibility in the future. The support for - regular (required) arguments is deprecated and will be removed in Rails 5.0 or later. +* Allow entries without a link tag in AtomFeedHelper. - *Piotr Chmolowski, Łukasz Strzałkowski* + *Daniel Gomez de Souza* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionview/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index d58dd9ed9b..3ec7a617cf 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/Rakefile b/actionview/Rakefile index d56fe9ea76..1b71435948 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -21,6 +21,7 @@ namespace :test do t.test_files = Dir.glob('test/template/**/*_test.rb').sort t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end namespace :integration do @@ -30,6 +31,7 @@ namespace :test do t.test_files = Dir.glob("test/activerecord/*_test.rb") t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end desc 'ActionPack Integration Tests' @@ -38,6 +40,7 @@ namespace :test do t.test_files = Dir.glob("test/actionpack/**/*_test.rb") t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 69c8326c51..8f9194cda7 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -23,8 +23,8 @@ 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' - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 6a1837c6e2..c3bbac27fd 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index d1bade0d3d..43124bb904 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -33,7 +33,9 @@ module ActionView #:nodoc: # # If you absolutely must write from within a function use +concat+. # - # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>. + # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace, + # including the trailing newline. <% %> and <%- -%> are the same. + # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces. # # === Using sub templates # @@ -68,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, @@ -124,8 +134,8 @@ module ActionView #:nodoc: # end # end # - # For more information on Builder please consult the [source - # code](https://github.com/jimweirich/builder). + # For more information on Builder please consult the {source + # code}[https://github.com/jimweirich/builder]. class Base include Helpers, ::ERB::Util, Context diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index 361a0dccbe..be5d86b1dc 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.rb @@ -13,10 +13,11 @@ module ActionView end alias :append= :<< - def safe_concat(value) - return self if value.nil? - super(value.to_s) + def safe_expr_append=(val) + return self if val.nil? + safe_concat val.to_s end + alias :safe_append= :safe_concat end 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/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 1f506866e9..4f45f5b8c8 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -5,10 +5,10 @@ module ActionView end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index b7fdc16a9d..5c28043f8a 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -318,6 +318,7 @@ 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} diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 9e8d005ec7..29733442c1 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -121,8 +121,8 @@ module ActionView # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) - return "" unless source.present? source = source.to_s + return "" unless source.present? return source if source =~ URI_REGEXP tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 227ad4cdfa..d8be4e5678 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -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..0e2a5f90f4 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -110,6 +110,29 @@ module ActionView # <%= some_helper_method(person) %> # # Now all you'll 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 like so: + # + # <% 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 safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) @@ -122,7 +145,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 +161,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 +184,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 75d1634b2e..5a3223968f 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -31,7 +31,8 @@ module ActionView # <head><title><%= @greeting %></title></head> # <body> # <b><%= @greeting %></b> - # </body></html> + # </body> + # </html> # def capture(*args) value = nil diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 9272bb5c10..46ce7cf0be 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -177,7 +177,9 @@ 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>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt>if + # * <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. # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if @@ -898,7 +900,7 @@ module ActionView def translated_date_order date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) - date_order = date_order.map { |element| element.to_sym } + date_order = date_order.map(&:to_sym) forbidden_elements = date_order - [:year, :month, :day] if forbidden_elements.any? @@ -1035,7 +1037,7 @@ module ActionView def build_selects_from_types(order) select = '' first_visible = order.find { |type| !@options[:"discard_#{type}"] } - order.reverse.each do |type| + order.reverse_each do |type| separator = separator(type) unless type == first_visible # don't add before first visible field select.insert(0, separator.to_s + send("select_#{type}").to_s) end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 09843ca70d..b76b35bf3c 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' @@ -51,9 +52,7 @@ module ActionView # The HTML generated for this would be (modulus formatting): # # <form action="/people" class="new_person" id="new_person" method="post"> - # <div style="display:none"> - # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> - # </div> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # <label for="person_first_name">First name</label>: # <input id="person_first_name" name="person[first_name]" type="text" /><br /> # @@ -81,10 +80,8 @@ module ActionView # the code above as is would yield instead: # # <form action="/people/256" class="edit_person" id="edit_person_256" method="post"> - # <div style="display:none"> - # <input name="_method" type="hidden" value="patch" /> - # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> - # </div> + # <input name="_method" type="hidden" value="patch" /> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # <label for="person_first_name">First name</label>: # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br /> # @@ -114,6 +111,7 @@ module ActionView include FormTagHelper include UrlHelper include ModelNaming + include RecordIdentifier # Creates a form that allows the user to create or update the attributes # of a specific model object. @@ -168,6 +166,23 @@ module ActionView # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of # id attributes on form elements. The namespace attribute will be prefixed # with underscore on the generated HTML id. + # * <tt>:method</tt> - The method to use when submitting the form, usually + # either "get" or "post". If "patch", "put", "delete", or another verb + # is used, a hidden input with name <tt>_method</tt> is added to + # simulate the verb over post. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. + # Use only if you need to pass custom authenticity token string, or to + # not add authenticity_token field at all (by passing <tt>false</tt>). + # Remote forms may omit the embedded authenticity token by setting + # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>. + # This is helpful when you're fragment-caching the form. Remote forms + # get the authenticity token from the <tt>meta</tt> tag, so embedding is + # unnecessary unless you support browsers without JavaScript. + # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive + # JavaScript drivers to control the submit behavior. By default this + # behavior is an ajax submit. + # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name + # utf8 is not output. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # # Also note that +form_for+ doesn't create an exclusive scope. It's still @@ -315,9 +330,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-remote='true'> - # <div style='display:none'> - # <input name='_method' type='hidden' value='patch' /> - # </div> + # <input name='_method' type='hidden' value='patch' /> # ... # </form> # @@ -333,9 +346,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'> - # <div style='display:none'> - # <input name='_method' type='hidden' value='patch' /> - # </div> + # <input name='_method' type='hidden' value='patch' /> # ... # </form> # @@ -428,6 +439,7 @@ module ActionView html_options[:data] = options.delete(:data) if options.has_key?(:data) html_options[:remote] = options.delete(:remote) if options.has_key?(:remote) html_options[:method] = options.delete(:method) if options.has_key?(:method) + html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8) html_options[:authenticity_token] = options.delete(:authenticity_token) builder = instantiate_builder(object_name, object, options) @@ -844,6 +856,24 @@ module ActionView # # file_field(:attachment, :file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> + # + # ==== Gotcha + # + # The HTML specification says that when a file field is empty, web browsers + # do not send any value to the server. Unfortunately this introduces a + # gotcha: if a +User+ model has an +avatar+ field, and no file is selected, + # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like + # + # @user.update(params[:user]) + # + # wouldn't update the +avatar+ field. + # + # To prevent this, the helper generates an auxiliary hidden field before + # every file field. The hidden field has the same name as the file one and + # a blank value. + # + # In case you don't want the helper to generate this hidden field you can + # specify the <tt>include_hidden: false</tt> option. def file_field(object_name, method, options = {}) Tags::FileField.new(object_name, method, self, options).render end @@ -1196,11 +1226,11 @@ 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 + def default_form_builder_class builder = ActionView::Base.default_form_builder builder.respond_to?(:constantize) ? builder.constantize : builder end @@ -1239,8 +1269,8 @@ module ActionView # end # # The above code creates a new method +div_radio_button+ which wraps a div - # around the a new radio button. Note that when options are passed in, you - # must called +objectify_options+ in order for the model object to get + # around the new radio button. Note that when options are passed in, you + # must call +objectify_options+ in order for the model object to get # correctly passed to the method. If +objectify_options+ is not called, # then the newly created helper will not be linked back to the model. # @@ -1925,6 +1955,8 @@ module ActionView end ActiveSupport.on_load(:action_view) do - cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder } + cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do + ::ActionView::Helpers::FormBuilder + end end end diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 83b07a00d4..bbfbf482a4 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -351,12 +351,12 @@ module ActionView return container if String === container selected, disabled = extract_selected_and_disabled(selected).map do |r| - Array(r).map { |item| item.to_s } + Array(r).map(&:to_s) end container.map do |element| html_attributes = option_html_attributes(element) - text, value = option_text_and_value(element).map { |item| item.to_s } + text, value = option_text_and_value(element).map(&:to_s) html_attributes[:selected] ||= option_value_selected?(value, selected) html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled) diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 7d1cdc5a68..65a0548ffb 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -84,14 +84,13 @@ module ActionView # * <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 @@ -133,12 +132,20 @@ module ActionView option_tags ||= "" html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name - if options.delete(:include_blank) - option_tags = content_tag(:option, '', :value => '').safe_concat(option_tags) + if options.include?(:include_blank) + include_blank = options.delete(:include_blank) + + if include_blank == true + include_blank = '' + end + + if include_blank + option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags) + end end if prompt = options.delete(:prompt) - option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags) + option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags) end content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) @@ -224,7 +231,7 @@ module ActionView # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')" # # type="hidden" value="" /> def hidden_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) + text_field_tag(name, value, options.merge(type: :hidden)) end # Creates a file upload field. If you are using file uploads then you will also need @@ -263,7 +270,7 @@ module ActionView # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html' # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" /> def file_field_tag(name, options = {}) - text_field_tag(name, nil, options.update("type" => "file")) + text_field_tag(name, nil, options.merge(type: :file)) end # Creates a password field, a masked text field that will hide the users input behind a mask character. @@ -296,7 +303,7 @@ module ActionView # password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input" # # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" /> def password_field_tag(name = "password", value = nil, options = {}) - text_field_tag(name, value, options.update("type" => "password")) + text_field_tag(name, value, options.merge(type: :password)) end # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. @@ -571,7 +578,7 @@ module ActionView # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" /> def color_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "color")) + text_field_tag(name, value, options.merge(type: :color)) end # Creates a text field of type "search". @@ -592,7 +599,7 @@ module ActionView # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" /> def search_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "search")) + text_field_tag(name, value, options.merge(type: :search)) end # Creates a text field of type "tel". @@ -613,7 +620,7 @@ module ActionView # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" /> def telephone_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "tel")) + text_field_tag(name, value, options.merge(type: :tel)) end alias phone_field_tag telephone_field_tag @@ -635,7 +642,7 @@ module ActionView # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" /> def date_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "date")) + text_field_tag(name, value, options.merge(type: :date)) end # Creates a text field of type "time". @@ -646,7 +653,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def time_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "time")) + text_field_tag(name, value, options.merge(type: :time)) end # Creates a text field of type "datetime". @@ -657,7 +664,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def datetime_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "datetime")) + text_field_tag(name, value, options.merge(type: :datetime)) end # Creates a text field of type "datetime-local". @@ -668,7 +675,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def datetime_local_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "datetime-local")) + text_field_tag(name, value, options.merge(type: 'datetime-local')) end # Creates a text field of type "month". @@ -679,7 +686,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def month_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "month")) + text_field_tag(name, value, options.merge(type: :month)) end # Creates a text field of type "week". @@ -690,7 +697,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def week_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "week")) + text_field_tag(name, value, options.merge(type: :week)) end # Creates a text field of type "url". @@ -711,7 +718,7 @@ module ActionView # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" /> def url_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "url")) + text_field_tag(name, value, options.merge(type: :url)) end # Creates a text field of type "email". @@ -732,7 +739,7 @@ module ActionView # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" /> def email_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "email")) + text_field_tag(name, value, options.merge(type: :email)) end # Creates a number field. @@ -769,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" /> @@ -790,7 +797,7 @@ module ActionView # ==== Options # * Accepts the same options as number_field_tag. def range_field_tag(name, value = nil, options = {}) - number_field_tag(name, value, options.stringify_keys.update("type" => "range")) + number_field_tag(name, value, options.merge(type: :range)) end # Creates the hidden UTF8 enforcer tag. Override this method in a helper @@ -862,7 +869,7 @@ module ActionView # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) - name.to_s.delete(']').gsub(/[^-a-zA-Z0-9:.]/, "_") + name.to_s.delete(']').tr('^-a-zA-Z0-9:.', "_") end end end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 7220bded3c..cfd617cedc 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' @@ -306,12 +305,12 @@ module ActionView # string containing an i18n scope where to find this hash. It # might have the following keys: # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, - # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, - # *<tt>:billion</tt>, <tt>:trillion</tt>, - # *<tt>:quadrillion</tt> + # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # <tt>:billion</tt>, <tt>:trillion</tt>, + # <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, - # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, - # *<tt>:pico</tt>, <tt>:femto</tt> + # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string # (defaults to "%n %u"). The field types are: # * %u - The quantifier (ex.: 'thousand') 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/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index e11670e00d..827932d8e2 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -32,7 +32,7 @@ module ActionView view_renderer.render(self, options) end else - view_renderer.render_partial(self, :partial => options, :locals => locals) + view_renderer.render_partial(self, :partial => options, :locals => locals, &block) end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 4f2db0a0c4..463a4e9f60 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/object/try' -require 'active_support/deprecation' require 'rails-html-sanitizer' module ActionView @@ -9,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 @@ -88,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! @@ -104,7 +102,7 @@ module ActionView self.class.full_sanitizer.sanitize(html) 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 @@ -167,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/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index c20800598f..a87c223a71 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -18,7 +18,7 @@ module ActionView itemscope allowfullscreen default inert sortable truespeed typemustmatch).to_set - BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym }) + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set @@ -123,7 +123,7 @@ module ActionView # cdata_section("hello]]>world") # # => <![CDATA[hello]]]]><![CDATA[>world]]> def cdata_section(content) - splitted = content.to_s.gsub(']]>', ']]]]><![CDATA[>') + splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>') "<![CDATA[#{splitted}]]>".html_safe end @@ -160,7 +160,7 @@ module ActionView attrs << tag_option(key, value, escape) end end - " #{attrs.sort! * ' '}" unless attrs.empty? + " #{attrs * ' '}" unless attrs.empty? end def prefix_tag_option(prefix, key, value, escape) 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 8607da301c..acc6443a96 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. @@ -25,19 +25,26 @@ module ActionView private def value(object) - object.send @method_name if object + object.public_send @method_name if object end def value_before_type_cast(object) 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 @@ -142,6 +144,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/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb index 476b820d84..e6a1d9c62d 100644 --- a/actionview/lib/action_view/helpers/tags/file_field.rb +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -2,6 +2,21 @@ module ActionView module Helpers module Tags # :nodoc: class FileField < TextField # :nodoc: + + def render + options = @options.stringify_keys + + if options.fetch("include_hidden", true) + add_default_name_and_id(options) + options[:type] = "file" + tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options) + else + options.delete("include_hidden") + @options = options + + super + end + end end 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/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb index c09e2f1be7..a848aeabfa 100644 --- a/actionview/lib/action_view/helpers/tags/search_field.rb +++ b/actionview/lib/action_view/helpers/tags/search_field.rb @@ -16,6 +16,7 @@ module ActionView options["incremental"] = true unless options.has_key?("incremental") end + @options = options super 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 b859653bc9..2c40ed1832 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -126,7 +126,7 @@ module ActionView text = sanitize(text) if options.fetch(:sanitize, true) if text.blank? || phrases.blank? - text + text || "" else match = Array(phrases).map do |p| Regexp === p ? p.to_s : Regexp.escape(p) @@ -309,7 +309,7 @@ module ActionView # <table> # <% @items.each do |item| %> # <tr class="<%= cycle("odd", "even") -%>"> - # <td>item</td> + # <td><%= item %></td> # </tr> # <% end %> # </table> diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index c2fda42396..342361217c 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -37,14 +37,17 @@ module ActionView # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) options = options.dup - options[:default] = wrap_translate_defaults(options[:default]) if options[:default] + remaining_defaults = Array(options.delete(:default)) + options[:default] = remaining_defaults.shift if remaining_defaults.first.kind_of? String - # If the user has specified rescue_format then pass it all through, otherwise use - # raise and do the work ourselves - options[:raise] ||= ActionView::Base.raise_on_missing_translations - - raise_error = options[:raise] || options.key?(:rescue_format) - unless raise_error + # 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?) + raise_error = false + options[:raise] = false + else + raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations options[:raise] = true end @@ -62,10 +65,14 @@ module ActionView I18n.translate(scope_key_by_partial(key), options) end rescue I18n::MissingTranslationData => e - raise e if raise_error + if remaining_defaults.present? + translate remaining_defaults.shift, options.merge(default: remaining_defaults) + else + raise e if raise_error - keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}") + keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) + content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}") + end end alias :t :translate @@ -94,21 +101,6 @@ module ActionView def html_safe_translation_key?(key) key.to_s =~ /(\b|_|\.)html$/ end - - def wrap_translate_defaults(defaults) - new_defaults = [] - defaults = Array(defaults) - while key = defaults.shift - if key.is_a?(Symbol) - new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) } - break - else - new_defaults << key - end - end - - new_defaults - end end end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index c3be47133c..3dbce0738e 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 @@ -229,59 +229,51 @@ module ActionView # ==== Examples # <%= button_to "New", action: "new" %> # # => "<form method="post" action="/controller/new" class="button_to"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # <%= button_to "New", new_articles_path %> # # => "<form method="post" action="/articles/new" class="button_to"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # <%= button_to [:make_happy, @user] do %> # Make happy <strong><%= @user.name %></strong> # <% end %> # # => "<form method="post" action="/users/1/make_happy" class="button_to"> - # # <div> - # # <button type="submit"> - # # Make happy <strong><%= @user.name %></strong> - # # </button> - # # </div> + # # <button type="submit"> + # # Make happy <strong><%= @user.name %></strong> + # # </button> # # </form>" # # <%= button_to "New", { action: "new" }, form_class: "new-thing" %> # # => "<form method="post" action="/controller/new" class="new-thing"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> - # # <div> - # # <input value="Create" type="submit" /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input value="Create" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # # <%= button_to "Delete Image", { action: "delete", id: @image.id }, # method: :delete, data: { confirm: "Are you sure?" } %> # # => "<form method="post" action="/images/delete/1" class="button_to"> - # # <div> - # # <input type="hidden" name="_method" value="delete" /> - # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input type="hidden" name="_method" value="delete" /> + # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # # <%= button_to('Destroy', 'http://www.example.com', # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %> # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'> - # # <div> - # # <input name='_method' value='delete' type='hidden' /> - # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input name='_method' value='delete' type='hidden' /> + # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # def button_to(name = nil, options = nil, html_options = nil, &block) @@ -436,6 +428,7 @@ module ActionView # * <tt>:body</tt> - Preset the body of the email. # * <tt>:cc</tt> - Carbon Copy additional recipients on the email. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. + # * <tt>:reply_to</tt> - Preset the Reply-To field of the email. # # ==== Obfuscation # Prior to Rails 4.0, +mail_to+ provided options for encoding the address @@ -465,9 +458,9 @@ module ActionView html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys - extras = %w{ cc bcc body subject }.map! { |item| - option = html_options.delete(item) || next - "#{item}=#{Rack::Utils.escape_path(option)}" + extras = %w{ cc bcc body subject reply_to }.map! { |item| + option = html_options.delete(item).presence || next + "#{item.dasherize}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + extras.join('&') diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 9ee05bd816..0b5c0b9991 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -262,7 +262,7 @@ module ActionView def layout(layout, conditions = {}) include LayoutConditions unless conditions.empty? - conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } + conditions.each {|k, v| conditions[k] = Array(v).map(&:to_s) } self._layout_conditions = conditions self._layout = layout diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index ea687d9cca..36855ec3d0 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -191,7 +191,6 @@ module ActionView def initialize(view_paths, details = {}, prefixes = []) @details, @details_key = {}, nil - @skip_default_locale = false @cache = true @prefixes = prefixes @rendered_format = nil @@ -213,12 +212,6 @@ module ActionView super(values) end - # Do not use the default locale on template lookup. - def skip_default_locale! - @skip_default_locale = true - self.locale = nil - end - # Override locale to return a symbol instead of array. def locale @details[:locale].first @@ -233,7 +226,7 @@ module ActionView config.locale = value end - super(@skip_default_locale ? I18n.locale : default_locale) + super(default_locale) end # Uses the first format in the formats array for layout lookup. 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..9a26cba574 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -36,9 +36,15 @@ 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 diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 63f645431a..6c6e69101b 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -2,29 +2,54 @@ require 'active_support/core_ext/module' require 'action_view/model_naming' module ActionView - # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or - # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to - # a higher logical level. + # RecordIdentifier encapsulates methods used by various ActionView helpers + # to associate records with DOM elements. # - # # routes - # resources :posts + # Consider for example the following code that displays the body of a post: # - # # view - # <%= div_for(post) do %> <div id="post_45" class="post"> - # <%= post.body %> What a wonderful world! - # <% end %> </div> + # <%= div_for(post) do %> + # <%= post.body %> + # <% end %> # - # # controller - # def update - # post = Post.find(params[:id]) - # post.update(params[:post]) + # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML + # is: # - # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post) - # end + # <div id="new_post" class="post"> + # </div> + # + # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML + # is: + # + # <div id="post_42" class="post"> + # What a wonderful world! + # </div> + # + # In both cases, the +id+ and +class+ of the wrapping DOM element are + # automatically generated, following naming conventions encapsulated by the + # RecordIdentifier methods #dom_id and #dom_class: + # + # dom_id(Post.new) # => "new_post" + # dom_class(Post.new) # => "post" + # dom_id(Post.find 42) # => "post_42" + # dom_class(Post.find 42) # => "post" # - # As the example above shows, you can stop caring to a large extent what the actual id of the post is. - # You just know that one is being assigned and that the subsequent calls in redirect_to expect that - # same naming convention and allows you to write less code if you follow it. + # Note that these methods do not strictly require +Post+ to be a subclass of + # ActiveRecord::Base. + # Any +Post+ class will work as long as its instances respond to +to_key+ + # and +model_name+, given that +model_name+ responds to +param_key+. + # For instance: + # + # class Post + # attr_accessor :to_key + # + # def model_name + # OpenStruct.new param_key: 'post' + # end + # + # def self.find(id) + # new.tap { |post| post.to_key = [id] } + # end + # end module RecordIdentifier extend self extend ModelNaming @@ -78,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 0407632435..56b8ab1e2d 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 @@ -73,7 +74,7 @@ module ActionView # # <%= render partial: "account", locals: { user: @buyer } %> # - # == Rendering a collection of partials + # == \Rendering a collection of partials # # The example of partial use describes a familiar pattern where a template needs to iterate over an array and # render a sub template for each of the elements. This pattern has been implemented as a single method that @@ -105,7 +106,7 @@ module ActionView # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also # just keep domain objects, like Active Records, in there. # - # == Rendering shared partials + # == \Rendering shared partials # # Two controllers can share a set of partials and render them like this: # @@ -113,7 +114,7 @@ module ActionView # # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. # - # == Rendering objects that respond to `to_partial_path` + # == \Rendering objects that respond to `to_partial_path` # # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work # and pick the proper path by checking `to_partial_path` method. @@ -127,7 +128,7 @@ module ActionView # # <%= render partial: "posts/post", collection: @posts %> # <%= render partial: @posts %> # - # == Rendering the default case + # == \Rendering the default case # # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand # defaults of render to render partials. Examples: @@ -147,7 +148,7 @@ module ActionView # # <%= render partial: "posts/post", collection: @posts %> # <%= render @posts %> # - # == Rendering partials with layouts + # == \Rendering partials with layouts # # Partials can have their own layouts applied to them. These layouts are different than the ones that are # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types @@ -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 @@ -366,10 +370,12 @@ module ActionView partial = options[:partial] if String === partial + @has_object = options.key?(:object) @object = options[:object] @collection = collection_from_options @path = partial else + @has_object = true @object = partial @collection = collection_from_object || collection_from_options @@ -382,7 +388,7 @@ module ActionView end if as = options[:as] - raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/ + raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/ as = as.to_sym end @@ -506,7 +512,7 @@ module ActionView def retrieve_template_keys keys = @locals.keys - keys << @variable if @object || @collection + keys << @variable if @has_object || @collection if @collection keys << @variable_counter keys << @variable_iteration @@ -517,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 @@ -528,11 +534,18 @@ module ActionView end IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + - "make sure your partial name starts with a lowercase letter or underscore, " + + "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, " + "and is followed by any combination of letters, numbers and underscores." def raise_invalid_identifier(path) raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) end + + def raise_invalid_option_as(as) + raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as)) + end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb new file mode 100644 index 0000000000..b77c884e66 --- /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 overriden 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/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index f3a48ecfa0..cd21d7ab47 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -18,7 +18,7 @@ module ActionView # Determine the template to be rendered using the given options. def determine_template(options) - keys = options.fetch(:locals, {}).keys + keys = options.has_key?(:locals) ? options[:locals].keys : [] if options.key?(:body) Template::Text.new(options[:body]) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 81d5836a8c..1e8e7415d1 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -35,13 +35,13 @@ module ActionView module ClassMethods def view_context_class @view_context_class ||= begin - include_path_helpers = supports_path? + supports_path = supports_path? routes = respond_to?(:_routes) && _routes helpers = respond_to?(:_helpers) && _helpers Class.new(ActionView::Base) do if routes - include routes.url_helpers(include_path_helpers) + include routes.url_helpers(supports_path) include routes.mounted_helpers end @@ -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. @@ -108,7 +111,7 @@ module ActionView end # Normalize args by converting render "foo" to render :action => "foo" and - # render "foo/bar" to render :file => "foo/bar". + # render "foo/bar" to render :template => "foo/bar". # :api: private def _normalize_args(action=nil, options={}) options = super(action, options) @@ -118,7 +121,7 @@ module ActionView options = action when String, Symbol action = action.to_s - key = action.include?(?/) ? :file : :action + key = action.include?(?/) ? :template : :action options[key] = action else options[:partial] = action diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 75febb8652..f281333a41 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -80,21 +80,38 @@ module ActionView when String options when nil - super({:only_path => true}) + super(only_path: _generate_paths_by_default) when Hash options = options.symbolize_keys - options[:only_path] = options[:host].nil? unless options.key?(:only_path) + unless options.key?(:only_path) + if options[:host].nil? + options[:only_path] = _generate_paths_by_default + else + options[:only_path] = false + end + end + super(options) when :back _back_url - when Symbol - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_string_call self, options when Array - polymorphic_path(options, options.extract_options!) - when Class - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_class_call self, options + if _generate_paths_by_default + polymorphic_path(options, options.extract_options!) + else + polymorphic_url(options, options.extract_options!) + end else - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call self, options + method = _generate_paths_by_default ? :path : :url + builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method) + + case options + when Symbol + builder.handle_string_call(self, options) + when Class + builder.handle_class_call(self, options) + else + builder.handle_model_call(self, options) + 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 eb0f2e8a57..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 @@ -313,20 +331,33 @@ module ActionView def locals_code #:nodoc: # Double assign to suppress the dreaded 'assigned but unused variable' warning - @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join + @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } end def method_name #:nodoc: - @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") + @method_name ||= begin + m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" + m.tr!('-', '_') + m + end end def identifier_method_name #:nodoc: - inspect.gsub(/[^a-z_]/, '_') + inspect.tr('^a-z_', '_') end def instrument(action, &block) 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/error.rb b/actionview/lib/action_view/template/error.rb index 743ef6de0a..390bce98a2 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -75,7 +75,7 @@ module ActionView def sub_template_message if @sub_templates "Trace of template inclusion: " + - @sub_templates.collect { |template| template.inspect }.join(", ") + @sub_templates.collect(&:inspect).join(", ") else "" end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index 33bfcb458c..0105e88a49 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -7,9 +7,9 @@ module ActionView #:nodoc: autoload :Raw, 'action_view/template/handlers/raw' def self.extended(base) - base.register_default_template_handler :erb, ERB.new + base.register_default_template_handler :raw, Raw.new + base.register_template_handler :erb, ERB.new base.register_template_handler :builder, Builder.new - base.register_template_handler :raw, Raw.new base.register_template_handler :ruby, :source.to_proc end @@ -22,7 +22,7 @@ module ActionView #:nodoc: # Register an object that knows how to handle template files with the given # extensions. This can be used to implement new template types. - # The handler must respond to `:call`, which will be passed the template + # The handler must respond to +:call+, which will be passed the template # and should return the rendered template as a String. def register_template_handler(*extensions, handler) raise(ArgumentError, "Extension is required") if extensions.empty? @@ -42,7 +42,7 @@ module ActionView #:nodoc: end def template_handler_extensions - @@template_handlers.keys.map {|key| key.to_s }.sort + @@template_handlers.keys.map(&:to_s).sort end def registered_template_handler(extension) diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 4523060442..88a8570706 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -35,7 +35,7 @@ module ActionView end end - BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expr_literal(src, code) flush_newline_if_pending(src) @@ -49,9 +49,9 @@ module ActionView def add_expr_escaped(src, code) flush_newline_if_pending(src) if code =~ BLOCK_EXPR - src << "@output_buffer.safe_append= " << code + src << "@output_buffer.safe_expr_append= " << code else - src << "@output_buffer.safe_append=(" << code << ");" + src << "@output_buffer.safe_expr_append=(" << code << ");" end end @@ -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 0c0d1fffcb..397c86014a 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(/:/, '\:') '%q:' + escaped + ':;' end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index d29d020c17..bc0db330ea 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -138,7 +138,7 @@ module ActionView # resolver is fresher before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info - locals = locals.map { |x| x.to_s }.sort! + locals = locals.map(&:to_s).sort! if key @cache.cache(key, name, prefix, partial, locals) do @@ -196,24 +196,12 @@ module ActionView } end - if RUBY_VERSION >= '2.2.0' - def find_template_paths(query) - Dir[query].reject { |filename| - File.directory?(filename) || - # deals with case-insensitive file systems. - !File.fnmatch(query, filename, File::FNM_EXTGLOB) - } - end - else - def find_template_paths(query) - # deals with case-insensitive file systems. - sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - - Dir[query].reject { |filename| - File.directory?(filename) || - !sanitizer[File.dirname(filename)].include?(filename) - } - end + def find_template_paths(query) + Dir[query].reject { |filename| + File.directory?(filename) || + # deals with case-insensitive file systems. + !File.fnmatch(query, filename, File::FNM_EXTGLOB) + } end # Helper for building query glob string based on resolver's pattern. @@ -250,11 +238,6 @@ module ActionView pieces.shift extension = pieces.pop - unless extension - message = "The file #{path} did not specify a template handler. The default is currently ERB, " \ - "but will change to RAW in the future." - ActiveSupport::Deprecation.warn message - end handler = Template.handler_for_extension(extension) format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last @@ -272,13 +255,13 @@ module ActionView # Default pattern, loads views the same way as previous versions of rails, eg. when you're # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}` # - # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") + # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # # This one allows you to keep files with different formats in separate subdirectories, # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. # - # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}") + # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # # If you don't specify a pattern then the default will be used. # @@ -287,7 +270,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" + # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" # ) # # ==== Pattern format and variables @@ -299,6 +282,7 @@ module ActionView # * <tt>:action</tt> - name of the action # * <tt>:locale</tt> - possible locale versions # * <tt>:formats</tt> - possible request formats (for example html, json, xml...) + # * <tt>:variants</tt> - possible request variants (for example phone, tablet...) # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) # class FileSystemResolver < PathResolver diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb index b84e0281ae..be45fcf742 100644 --- a/actionview/lib/action_view/template/types.rb +++ b/actionview/lib/action_view/template/types.rb @@ -9,7 +9,7 @@ module ActionView self.types = Set.new def self.register(*t) - types.merge(t.map { |type| type.to_s }) + types.merge(t.map(&:to_s)) end register :html, :text, :js, :css, :xml, :json diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index af34d2ce5a..06810ad14d 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -125,6 +125,7 @@ module ActionView @_rendered_views ||= RenderedViewsCollection.new end + # Need to experiment if this priority is the best one: rendered => output_buffer class RenderedViewsCollection def initialize @rendered_views ||= Hash.new { |hash, key| hash[key] = [] } @@ -203,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/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 80a41f2418..492f67f45d 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -16,14 +16,9 @@ module ActionView module ClassMethods def _prefixes # :nodoc: @_prefixes ||= begin - deprecated_prefixes = handle_deprecated_parent_prefixes - if deprecated_prefixes - deprecated_prefixes - else - return local_prefixes if superclass.abstract? - - local_prefixes + superclass._prefixes - end + return local_prefixes if superclass.abstract? + + local_prefixes + superclass._prefixes end end @@ -34,13 +29,6 @@ module ActionView def local_prefixes [controller_path] end - - def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0. - return unless respond_to?(:parent_prefixes) - - ActiveSupport::Deprecation.warn "Overriding ActionController::Base::parent_prefixes is deprecated, override .local_prefixes instead." - local_prefixes + parent_prefixes - end end # The prefixes used in render "foo" shortcuts. diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index e51461d82c..fc1ca9efdf 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -16,11 +16,9 @@ silence_warnings do end require 'active_support/testing/autorun' -require 'abstract_controller' require 'action_controller' require 'action_view' require 'action_view/testing/resolvers' -require 'action_dispatch' require 'active_support/dependencies' require 'active_model' require 'active_record' @@ -48,7 +46,7 @@ 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 {|locale| locale.to_s }.sort +ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') FIXTURES = Pathname.new(FIXTURE_LOAD_PATH) @@ -270,7 +268,7 @@ class Rack::TestCase < ActionDispatch::IntegrationTest end end -ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) +ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) module ActionController class Base diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb index e653b12d32..490932fef0 100644 --- a/actionview/test/actionpack/abstract/abstract_controller_test.rb +++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb @@ -168,7 +168,7 @@ module AbstractController end end - class OverridingLocalPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3. + class OverridingLocalPrefixesTest < ActiveSupport::TestCase test "overriding .local_prefixes adds prefix" do @controller = OverridingLocalPrefixes.new @controller.process(:index) @@ -182,22 +182,6 @@ module AbstractController end end - class DeprecatedParentPrefixes < OverridingLocalPrefixes - def self.parent_prefixes - ["abstract_controller/testing/me3"] - end - end - - class DeprecatedParentPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3. - test "overriding .parent_prefixes is deprecated" do - @controller = DeprecatedParentPrefixes.new - assert_deprecated do - @controller.process(:index) - end - assert_equal "Hello from me3/index.erb", @controller.response_body - end - end - # Test rendering with layouts # ==== # self._layout is used when defined diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index bd345fe873..7b8a83e2fe 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 diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index b3b51ae583..8b47536a18 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -31,6 +31,10 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end + + def cache_key + name.to_s + end end module Quiz @@ -91,17 +95,17 @@ class TestController < ApplicationController # :ported: def render_hello_world - render :template => "test/hello_world" + render "test/hello_world" end def render_hello_world_with_last_modified_set response.last_modified = Date.new(2008, 10, 10).to_time - render :template => "test/hello_world" + render "test/hello_world" end # :ported: compatibility def render_hello_world_with_forward_slash - render :template => "/test/hello_world" + render "/test/hello_world" end # :ported: @@ -111,7 +115,7 @@ class TestController < ApplicationController # :deprecated: def render_template_in_top_directory_with_slash - render :template => '/shared' + render '/shared' end # :ported: @@ -160,13 +164,6 @@ class TestController < ApplicationController end # :ported: - def render_file_as_string_with_instance_variables - @secret = 'in the sauce' - path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')) - render path - end - - # :ported: def render_file_not_using_full_path @secret = 'in the sauce' render :file => 'test/render_file_with_ivar' @@ -194,7 +191,7 @@ class TestController < ApplicationController def render_file_as_string_with_locals path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')) - render path, :locals => {:secret => 'in the sauce'} + render file: path, :locals => {:secret => 'in the sauce'} end def accessing_request_in_template @@ -460,6 +457,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 @@ -794,12 +795,6 @@ class RenderTest < ActionController::TestCase end # :ported: - def test_render_file_as_string_with_instance_variables - get :render_file_as_string_with_instance_variables - assert_equal "The secret is in the sauce\n", @response.body - end - - # :ported: def test_render_file_not_using_full_path get :render_file_not_using_full_path assert_equal "The secret is in the sauce\n", @response.body @@ -870,12 +865,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: @@ -966,23 +961,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 @@ -1055,7 +1050,7 @@ class RenderTest < ActionController::TestCase 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 @@ -1115,6 +1110,11 @@ class RenderTest < ActionController::TestCase assert_equal "world", assigns["hello"] end + def test_render_text_with_assigns_option + get :render_with_assigns_option + assert_equal 'world', response.body + end + # :ported: def test_template_with_locals get :render_with_explicit_template_with_locals diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index c6e7a523b9..7fba9ff8ff 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -39,7 +39,7 @@ class ViewLoadPathsTest < ActionController::TestCase def assert_paths(*paths) controller = paths.first.is_a?(Class) ? paths.shift : @controller - assert_equal expand(paths), controller.view_paths.map { |p| p.to_s } + assert_equal expand(paths), controller.view_paths.map(&:to_s) end def test_template_load_path_was_set_correctly 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/template/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb index 5609694cd5..5609694cd5 100644 --- a/actionview/test/template/debug_helper_test.rb +++ b/actionview/test/activerecord/debug_helper_test.rb diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index 4e94304796..6b51775cf3 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -25,15 +25,17 @@ class Series < ActiveRecord::Base self.table_name = 'projects' end -class ModelDelegator < ActiveRecord::Base - self.table_name = 'projects' - +class ModelDelegator def to_model ModelDelegate.new end end class ModelDelegate + def persisted? + true + end + def model_name ActiveModel::Name.new(self.class) end @@ -111,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 @@ -282,6 +284,15 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def test_regression_path_helper_prefixed_with_new_and_edit + with_test_routes do + assert_equal "/projects/new", new_polymorphic_path(@project) + + @project.save + assert_equal "/projects/#{@project.id}/edit", edit_polymorphic_path(@project) + end + end + def test_url_helper_prefixed_with_edit with_test_routes do @project.save @@ -596,13 +607,18 @@ class PolymorphicRoutesTest < ActionController::TestCase end end - def test_routing_a_to_model_delegate + def test_routing_to_a_model_delegate with_test_routes do - @delegator.save assert_url "http://example.com/model_delegates/overridden", @delegator end end + def test_nested_routing_to_a_model_delegate + with_test_routes do + assert_url "http://example.com/foo/model_delegates/overridden", [:foo, @delegator] + end + end + def with_namespaced_routes(name) with_routing do |set| set.draw do @@ -636,6 +652,9 @@ class PolymorphicRoutesTest < ActionController::TestCase end resources :series resources :model_delegates + namespace :foo do + resources :model_delegates + end end extend @routes.url_helpers diff --git a/actionview/test/fixtures/blog_public/.gitignore b/actionview/test/fixtures/blog_public/.gitignore deleted file mode 100644 index 312e635ee6..0000000000 --- a/actionview/test/fixtures/blog_public/.gitignore +++ /dev/null @@ -1 +0,0 @@ -absolute/* diff --git a/actionview/test/fixtures/blog_public/blog.html b/actionview/test/fixtures/blog_public/blog.html deleted file mode 100644 index 79ad44c010..0000000000 --- a/actionview/test/fixtures/blog_public/blog.html +++ /dev/null @@ -1 +0,0 @@ -/blog/blog.html
\ No newline at end of file diff --git a/actionview/test/fixtures/blog_public/index.html b/actionview/test/fixtures/blog_public/index.html deleted file mode 100644 index 2de3825481..0000000000 --- a/actionview/test/fixtures/blog_public/index.html +++ /dev/null @@ -1 +0,0 @@ -/blog/index.html
\ No newline at end of file diff --git a/actionview/test/fixtures/blog_public/subdir/index.html b/actionview/test/fixtures/blog_public/subdir/index.html deleted file mode 100644 index 517bded335..0000000000 --- a/actionview/test/fixtures/blog_public/subdir/index.html +++ /dev/null @@ -1 +0,0 @@ -/blog/subdir/index.html
\ No newline at end of file diff --git a/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb deleted file mode 100644 index 3125583a28..0000000000 --- a/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<body> -<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %> -</body> diff --git a/actionview/test/fixtures/happy_path/render_action/hello_world.erb b/actionview/test/fixtures/happy_path/render_action/hello_world.erb deleted file mode 100644 index 6769dd60bd..0000000000 --- a/actionview/test/fixtures/happy_path/render_action/hello_world.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world!
\ No newline at end of file diff --git a/actionview/test/fixtures/scope/test/modgreet.erb b/actionview/test/fixtures/scope/test/modgreet.erb deleted file mode 100644 index 8947726e89..0000000000 --- a/actionview/test/fixtures/scope/test/modgreet.erb +++ /dev/null @@ -1 +0,0 @@ -<p>Beautiful modules!</p>
\ No newline at end of file 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/_label_with_block.erb b/actionview/test/fixtures/test/_label_with_block.erb index 40117e594e..94089ea93d 100644 --- a/actionview/test/fixtures/test/_label_with_block.erb +++ b/actionview/test/fixtures/test/_label_with_block.erb @@ -1,4 +1,4 @@ -<%= label 'post', 'message' do %> +<%= label('post', 'message')do %> Message <%= text_field 'post', 'message' %> <% end %> diff --git a/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb b/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb new file mode 100644 index 0000000000..352128f3ba --- /dev/null +++ b/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb @@ -0,0 +1,3 @@ +<%= render "test/layout_for_block_with_args" do |arg_1, arg_2| %> + Yielded: <%= arg_1 %>/<%= arg_2 %> +<% end %> diff --git a/actionview/test/fixtures/test/nil_return.erb b/actionview/test/fixtures/test/nil_return.erb new file mode 100644 index 0000000000..90ce3881f6 --- /dev/null +++ b/actionview/test/fixtures/test/nil_return.erb @@ -0,0 +1 @@ +This is nil: <%== nil %> diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index a463a08bb6..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 @@ -111,19 +127,6 @@ class CommentRelevance end end -class Sheep - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - def to_key; id ? [id] : nil end - def save; @id = 1 end - def new_record?; @id.nil? end - def name - @id.nil? ? 'new sheep' : "sheep ##{@id}" - end -end - class TagRelevance extend ActiveModel::Naming include ActiveModel::Conversion @@ -183,3 +186,15 @@ end class Car < Struct.new(:color) end + +class Plane + attr_reader :to_key + + def model_name + OpenStruct.new param_key: 'airplane' + end + + def save + @to_key = [1] + end +end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index d789a5ca27..a15c6fac90 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -180,6 +180,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 +239,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>), @@ -302,7 +304,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_autodiscovery_link_tag_with_unknown_type result = auto_discovery_link_tag(:xml, '/feed.xml', :type => 'application/xml') expected = %(<link href="/feed.xml" rel="alternate" title="XML" type="application/xml" />) - assert_equal expected, result + assert_dom_equal expected, result end def test_asset_path_tag @@ -535,6 +537,17 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal copy, source end + class PlaceholderImage + def blank?; true; end + def to_s; 'no-image-yet.png'; end + end + def test_image_tag_with_blank_placeholder + assert_equal '<img alt="" src="/images/no-image-yet.png" />', image_tag(PlaceholderImage.new, alt: "") + end + def test_image_path_with_blank_placeholder + assert_equal '/images/no-image-yet.png', image_path(PlaceholderImage.new) + end + def test_image_path_with_asset_host_proc_returning_nil @controller.config.asset_host = Proc.new do |source| unless source.end_with?("tiff") diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 68b44c4f0d..525d99750d 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -62,6 +62,23 @@ 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!") @@ -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/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb index b84aca6746..f6c1283b92 100644 --- a/actionview/test/template/compiled_templates_test.rb +++ b/actionview/test/template/compiled_templates_test.rb @@ -5,6 +5,10 @@ class CompiledTemplatesTest < ActiveSupport::TestCase ActionView::LookupContext::DetailsKey.clear end + def test_template_with_nil_erb_return + assert_equal "This is nil: \n", render(:template => "test/nil_return") + end + def test_template_gets_recompiled_when_using_different_keys_in_local_assigns assert_equal "one", render(:file => "test/render_file_with_locals_and_default") assert_equal "two", render(:file => "test/render_file_with_locals_and_default", :locals => { :secret => "two" }) diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 0cdb130710..bfb073680e 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -1504,7 +1504,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" - assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true, :include_seconds => true, + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) end @@ -2330,7 +2330,7 @@ class DateHelperTest < ActionView::TestCase # The love zone is UTC+0 mytz = Class.new(ActiveSupport::TimeZone) { attr_accessor :now - }.create('tenderlove', 0) + }.create('tenderlove', 0, ActiveSupport::TimeZone.find_tzinfo('UTC')) now = Time.mktime(2004, 6, 15, 16, 35, 0) mytz.now = now 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/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 3bb84cbc50..3e72be31de 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -84,7 +84,7 @@ class ErbUtilTest < ActiveSupport::TestCase end def test_rest_in_ascii - (0..127).to_a.map {|int| int.chr }.each do |chr| + (0..127).to_a.map(&:chr).each do |chr| next if %('"&<>).include?(chr) assert_equal chr, html_escape(chr) end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 6f82462425..4e336bea63 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 @@ -329,12 +353,28 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_block_in_erb - assert_equal( + assert_dom_equal( %{<label for="post_message">\n Message\n <input id="post_message" name="post[message]" type="text" />\n</label>}, view.render("test/label_with_block") ) 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?")) @@ -469,22 +525,36 @@ class FormHelperTest < ActionView::TestCase def test_text_field_doesnt_change_param_values object_name = 'post[]' expected = '<input id="post_123_title" name="post[123][title]" type="text" value="Hello World" />' - assert_equal expected, text_field(object_name, "title") - assert_equal object_name, "post[]" + assert_dom_equal expected, text_field(object_name, "title") end - def test_file_field_has_no_size + def test_file_field_does_generate_a_hidden_field + expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />' + assert_dom_equal expected, file_field("user", "avatar") + end + + def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false + expected = '<input id="user_avatar" name="user[avatar]" type="file" />' + assert_dom_equal expected, file_field("user", "avatar", include_hidden: false) + end + + def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false_with_key_as_string expected = '<input id="user_avatar" name="user[avatar]" type="file" />' + assert_dom_equal expected, file_field("user", "avatar", "include_hidden" => false) + end + + def test_file_field_has_no_size + expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />' assert_dom_equal expected, file_field("user", "avatar") end def test_file_field_with_multiple_behavior - expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />' + expected = '<input name="import[file][]" type="hidden" value="" /><input id="import_file" multiple="multiple" name="import[file][]" type="file" />' assert_dom_equal expected, file_field("import", "file", :multiple => true) end def test_file_field_with_multiple_behavior_and_explicit_name - expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />' + expected = '<input name="custom" type="hidden" value="" /><input id="import_file" multiple="multiple" name="custom" type="file" />' assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom") end @@ -886,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]">\n123</textarea>}, + %{<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]">\nomg</textarea>}, text_area("post", "id") ) end @@ -929,6 +1019,11 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, search_field("contact", "notes_query")) end + def test_search_field_with_onsearch_value + expected = %{<input onsearch="true" type="search" name="contact[notes_query]" id="contact_notes_query" incremental="true" />} + assert_dom_equal(expected, search_field("contact", "notes_query", onsearch: true)) + end + def test_telephone_field expected = %{<input id="user_cell" name="user[cell]" type="tel" />} assert_dom_equal(expected, telephone_field("user", "cell")) @@ -1715,7 +1810,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do - "<input name='post[file]' type='file' id='post_file' />" + "<input name='post[file]' type='hidden' value='' /><input name='post[file]' type='file' id='post_file' />" end assert_dom_equal expected, output_buffer @@ -1731,7 +1826,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do - "<input name='post[comment][file]' type='file' id='post_comment_file' />" + "<input name='post[comment][file]' type='hidden' value='' /><input name='post[comment][file]' type='file' id='post_comment_file' />" end assert_dom_equal expected, output_buffer @@ -1786,6 +1881,20 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_tags_do_not_call_private_properties_on_form_object + obj = Class.new do + private + + def private_property + raise "This method should not be called." + end + end.new + + form_for(obj, as: "other_name", url: '/', html: { id: "edit-other-name" }) do |f| + assert_raise(NoMethodError) { f.hidden_field(:private_property) } + end + end + def test_form_for_with_method_as_part_of_html_options form_for(@post, url: '/', html: { id: 'create-post', method: :delete }) do |f| concat f.text_field(:title) @@ -1851,6 +1960,30 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_enforce_utf8_true + form_for(:post, enforce_utf8: true) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", nil, nil, enforce_utf8: true) do + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_enforce_utf8_false + form_for(:post, enforce_utf8: false) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", nil, nil, enforce_utf8: false) do + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_remote_in_html form_for(@post, url: '/', html: { remote: true, id: 'create-post', method: :patch }) do |f| concat f.text_field(:title) @@ -3196,7 +3329,7 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_string_url_option form_for(@post, url: 'http://www.otherdomain.com') do |f| end - assert_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer + assert_dom_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer end def test_form_for_with_hash_url_option @@ -3210,14 +3343,14 @@ class FormHelperTest < ActionView::TestCase form_for(@post, url: @post) do |f| end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") - assert_equal expected, output_buffer + assert_dom_equal expected, output_buffer end def test_form_for_with_existing_object form_for(@post) do |f| end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") - assert_equal expected, output_buffer + assert_dom_equal expected, output_buffer end def test_form_for_with_new_object @@ -3228,7 +3361,7 @@ class FormHelperTest < ActionView::TestCase form_for(post) do |f| end expected = whole_form("/posts", "new_post", "new_post") - assert_equal expected, output_buffer + assert_dom_equal expected, output_buffer end def test_form_for_with_existing_object_in_list @@ -3265,7 +3398,7 @@ class FormHelperTest < ActionView::TestCase form_for(@post, url: "/super_posts") do |f| end expected = whole_form("/super_posts", "edit_post_123", "edit_post", method: "patch") - assert_equal expected, output_buffer + assert_dom_equal expected, output_buffer end def test_form_for_with_default_method_as_patch @@ -3300,8 +3433,14 @@ class FormHelperTest < ActionView::TestCase protected - def hidden_fields(method = nil) - txt = %{<input name="utf8" type="hidden" value="✓" />} + def hidden_fields(options = {}) + method = options[:method] + + if options.fetch(:enforce_utf8, true) + txt = %{<input name="utf8" type="hidden" value="✓" />} + else + txt = '' + end if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} @@ -3325,7 +3464,7 @@ class FormHelperTest < ActionView::TestCase method, remote, multipart = options.values_at(:method, :remote, :multipart) - form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>" + form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>" end def protect_against_forgery? diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 771e3fefc3..84a581b107 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -170,6 +170,13 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_multiple_field_tags_with_same_options + options = {class: 'important'} + assert_dom_equal %(<input name="title" type="file" id="title" class="important"/>), file_field_tag("title", options) + assert_dom_equal %(<input type="password" name="title" id="title" value="Hello!" class="important" />), password_field_tag("title", "Hello!", options) + assert_dom_equal %(<input type="text" name="title" id="title" value="Hello!" class="important" />), text_field_tag("title", "Hello!", options) + end + def test_radio_button_tag actual = radio_button_tag "people", "david" expected = %(<input id="people_david" name="people" type="radio" value="david" />) @@ -225,6 +232,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_select_tag_with_include_blank_false + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: false + expected = %(<select id="places" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_with_include_blank_string + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: 'Choose' + expected = %(<select id="places" name="places"><option value="">Choose</option><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + def test_select_tag_with_prompt actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string" expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>) diff --git a/actionview/test/template/record_identifier_test.rb b/actionview/test/template/record_identifier_test.rb index 22038110a5..04898c0b0e 100644 --- a/actionview/test/template/record_identifier_test.rb +++ b/actionview/test/template/record_identifier_test.rb @@ -9,7 +9,6 @@ class RecordIdentifierTest < ActiveSupport::TestCase @record = @klass.new @singular = 'comment' @plural = 'comments' - @uncountable = Sheep end def test_dom_id_with_new_record @@ -47,3 +46,46 @@ class RecordIdentifierTest < ActiveSupport::TestCase assert_equal @singular, ActionView::RecordIdentifier.dom_class(@record) end end + +class RecordIdentifierWithoutActiveModelTest < ActiveSupport::TestCase + include ActionView::RecordIdentifier + + def setup + @record = Plane.new + end + + def test_dom_id_with_new_record + assert_equal "new_airplane", dom_id(@record) + end + + def test_dom_id_with_new_record_and_prefix + assert_equal "custom_prefix_airplane", dom_id(@record, :custom_prefix) + end + + def test_dom_id_with_saved_record + @record.save + assert_equal "airplane_1", dom_id(@record) + end + + def test_dom_id_with_prefix + @record.save + assert_equal "edit_airplane_1", dom_id(@record, :edit) + end + + def test_dom_class + assert_equal 'airplane', dom_class(@record) + end + + def test_dom_class_with_prefix + assert_equal "custom_prefix_airplane", dom_class(@record, :custom_prefix) + end + + def test_dom_id_as_singleton_method + @record.save + assert_equal "airplane_1", ActionView::RecordIdentifier.dom_id(@record) + end + + def test_dom_class_as_singleton_method + assert_equal 'airplane', ActionView::RecordIdentifier.dom_class(@record) + end +end 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 85817119ba..22665b6844 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' @@ -16,7 +15,7 @@ module RenderTestCases I18n.backend.store_translations 'pt-BR', {} # Ensure original are still the same since we are reindexing view paths - assert_equal ORIGINAL_LOCALES, I18n.available_locales.map {|l| l.to_s }.sort + assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort end def test_render_without_options @@ -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 a lowercase letter or 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 a lowercase letter or 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,10 +185,25 @@ 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 a lowercase letter or underscore, " + + assert_nothing_raised { @view.render(:partial => "test/a-in") } + end + + def test_render_partial_with_invalid_option_as + e = assert_raises(ArgumentError) { @view.render(:partial => "test/partial_only", :as => 'a-in') } + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + + "make sure it starts with lowercase letter, " + + "and is followed by any combination of letters, numbers and underscores.", e.message + end + + def test_render_partial_with_hyphen_and_invalid_option_as + e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in", :as => 'a-in') } + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + + "make sure it starts with lowercase letter, " + "and is followed by any combination of letters, numbers and underscores.", e.message end @@ -466,6 +475,11 @@ module RenderTestCases @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) end + def test_render_partial_shortcut_with_block_content + assert_equal %(Before (shortcut test)\nBefore\n\n Yielded: arg1/arg2\n\nAfter\nAfter), + @view.render(partial: "test/partial_shortcut_with_block_content", layout: "test/layout_for_partial", locals: { name: "shortcut test" }) + end + def test_render_layout_with_a_nested_render_layout_call assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter), @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) @@ -584,3 +598,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/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index 8a24d78e74..ec537775be 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 diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index ce89d5728e..d037447567 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -50,6 +50,11 @@ class TagHelperTest < ActionView::TestCase assert_dom_equal "<div>Hello world!</div>", buffer end + def test_content_tag_with_block_in_erb_containing_non_displayed_erb + buffer = render_erb("<%= content_tag(:p) do %><% 1 %><% end %>") + assert_dom_equal "<p></p>", buffer + end + def test_content_tag_with_block_and_options_in_erb buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>") assert_dom_equal %(<div class="green">Hello world!</div>), buffer @@ -64,6 +69,11 @@ class TagHelperTest < ActionView::TestCase content_tag("a", "href" => "create") { "Create" } end + def test_content_tag_with_block_and_non_string_outside_out_of_erb + assert_equal content_tag("p"), + content_tag("p") { 3.times { "do_something" } } + end + def test_content_tag_nested_in_content_tag_out_of_erb assert_equal content_tag("p", content_tag("b", "Hello")), content_tag("p") { content_tag("b", "Hello") }, 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 5697ffa317..c6cc47fb4f 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -155,7 +155,7 @@ module ActionView test "view_assigns excludes internal ivars" do INTERNAL_IVARS.each do |ivar| assert defined?(ivar), "expected #{ivar} to be defined" - assert !view_assigns.keys.include?(ivar.to_s.sub('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns" + assert !view_assigns.keys.include?(ivar.to_s.tr('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns" end end end @@ -328,9 +328,10 @@ module ActionView 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 + e = assert_raise ActiveSupport::TestCase::Assertion do assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" } end + assert_match(/Somebody Else.*David/m, e.message) end test 'supports different locals on the same partial' do diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index 667f9002da..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 @@ -193,6 +192,10 @@ class TextHelperTest < ActionView::TestCase assert_equal ' ', highlight(' ', 'blank text is returned verbatim') end + def test_highlight_should_return_blank_string_for_nil + assert_equal '', highlight(nil, 'blank string is returned for nil') + end + def test_highlight_should_sanitize_input assert_equal( "This is a <mark>beautiful</mark> morning", diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index 362f05ea70..8fde478ac9 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -1,5 +1,13 @@ require 'abstract_unit' +module I18n + class CustomExceptionHandler + def self.call(exception, locale, key, options) + 'from CustomExceptionHandler' + end + end +end + class TranslationHelperTest < ActiveSupport::TestCase include ActionView::Helpers::TranslationHelper @@ -72,6 +80,22 @@ class TranslationHelperTest < ActiveSupport::TestCase end end + def test_uses_custom_exception_handler_when_specified + old_exception_handler = I18n.exception_handler + I18n.exception_handler = I18n::CustomExceptionHandler + assert_equal 'from CustomExceptionHandler', translate(:"translations.missing", raise: false) + ensure + I18n.exception_handler = old_exception_handler + end + + def test_uses_custom_exception_handler_when_specified_for_html + old_exception_handler = I18n.exception_handler + I18n.exception_handler = I18n::CustomExceptionHandler + assert_equal 'from CustomExceptionHandler', translate(:"translations.missing_html", raise: false) + ensure + 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") @@ -145,6 +169,12 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translation.html_safe? end + def test_translate_with_last_default_not_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.foo']) + assert_equal 'Foo', translation + assert_equal false, translation.html_safe? + end + def test_translate_with_string_default translation = translate(:'translations.missing', default: 'A Generic String') assert_equal 'A Generic String', translation diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index e0678ae1f7..ef4df0407a 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' @@ -493,8 +492,13 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_with_options assert_dom_equal( - %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&bcc=bccaddress%40example.com&body=This%20is%20the%20body%20of%20the%20message.&subject=This%20is%20an%20example%20email">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.") + %{<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 @@ -624,13 +628,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 +650,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 +665,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 +708,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 +782,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 b04883413e..d4e19274fa 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1 +1,80 @@ -* Started project.
\ No newline at end of file +* 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. + + Example: + + 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. + + 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 + + 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 + + *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/MIT-LICENSE b/activejob/MIT-LICENSE index 8b1e97b776..0cef8cdda0 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 David Heinemeier Hansson +Copyright (c) 2014-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activejob/README.md b/activejob/README.md index 5734ca413e..8c83d3669a 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -5,7 +5,7 @@ of queueing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really. -It also serves as the backend for ActionMailer's #deliver_later functionality +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 of the request-response cycle, so the user doesn't have to wait on it. @@ -24,9 +24,10 @@ Set the queue adapter for Active Job: ``` ruby ActiveJob::Base.queue_adapter = :inline # default queue adapter -# Adapters currently supported: :backburner, :delayed_job, :qu, :que, :queue_classic, -# :resque, :sidekiq, :sneakers, :sucker_punch ``` +Note: To learn how to use your preferred queueing backend see its adapter +documentation at +[ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). Declare a job like so: @@ -88,18 +89,9 @@ by default has been mixed into Active Record classes. ## Supported queueing systems -We currently have adapters for: - -* [Backburner](https://github.com/nesquena/backburner) -* [Delayed Job](https://github.com/collectiveidea/delayed_job) -* [Qu](https://github.com/bkeepers/qu) -* [Que](https://github.com/chanks/que) -* [QueueClassic](https://github.com/ryandotsmith/queue_classic) -* [Resque 1.x](https://github.com/resque/resque) -* [Sidekiq](https://github.com/mperham/sidekiq) -* [Sneakers](https://github.com/jondot/sneakers) -* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch) - +Active Job has built-in adapters for multiple queueing backends (Sidekiq, +Resque, Delayed Job and others). To get an up-to-date list of the adapters +see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). ## Auxiliary gems @@ -119,7 +111,7 @@ Source code can be downloaded as part of the Rails project on GitHub ## License -ActiveJob is released under the MIT license: +Active Job is released under the MIT license: * http://www.opensource.org/licenses/MIT @@ -137,5 +129,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - - diff --git a/activejob/Rakefile b/activejob/Rakefile index e918428459..6c7e6aae6e 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,59 +1,47 @@ require 'rake/testtask' require 'rubygems/package_task' -dir = File.dirname(__FILE__) - -def run_without_aborting(*tasks) - errors = [] - - tasks.each do |task| - begin - Rake::Task[task].invoke - rescue Exception - errors << task - end - end - - abort "Errors running #{errors.join(', ')}" if errors.any? -end - -task default: :test - -ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner) +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) -desc 'Run all adapter tests' -task :test do - tasks = ACTIVEJOB_ADAPTERS.map{|a| "test_#{a}" } - run_without_aborting(*tasks) -end +task default: :test +task test: 'test:default' namespace :test do + desc 'Run all adapter tests' + task :default do + run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:#{a}" } + end + desc 'Run all adapter tests in isolation' task :isolated do - tasks = ACTIVEJOB_ADAPTERS.map{|a| "isolated_test_#{a}" } - run_without_aborting(*tasks) + run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:isolated:#{a}" } end - desc 'Run all adapter integration tests' + desc 'Run integration tests for all adapters' task :integration do - tasks = ACTIVEJOB_ADAPTERS.map{|a| "integration_test_#{a}" } - run_without_aborting(*tasks) + run_without_aborting (ACTIVEJOB_ADAPTERS - ['test']).map { |a| "test:integration:#{a}" } + end + + task 'env:integration' do + ENV['AJ_INTEGRATION_TESTS'] = "1" end -end + ACTIVEJOB_ADAPTERS.each do |adapter| + task("env:#{adapter}") { ENV['AJ_ADAPTER'] = adapter } -ACTIVEJOB_ADAPTERS.each do |adapter| - namespace :test do - Rake::TestTask.new(adapter => "#{adapter}:env") do |t| - t.description = "" + 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.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end namespace :isolated do - task adapter => "#{adapter}:env" do + task adapter => "test:env:#{adapter}" do + dir = File.dirname(__FILE__) Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file| sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file) end or raise 'Failures' @@ -61,42 +49,30 @@ ACTIVEJOB_ADAPTERS.each do |adapter| end namespace :integration do - Rake::TestTask.new(adapter => "#{adapter}:env") do |t| - t.description = "" + Rake::TestTask.new(adapter => ["test:env:#{adapter}", 'test:env:integration']) do |t| + t.description = "Run integration tests for #{adapter}" t.libs << 'test' t.test_files = FileList['test/integration/**/*_test.rb'] t.verbose = true + t.warning = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end end +end - namespace adapter do - task test: "test_#{adapter}" - task isolated_test: "isolated_test_#{adapter}" - - task(:env) { ENV['AJADAPTER'] = adapter } - - namespace :isolated do - task(:env) { ENV['AJADAPTER'] = adapter } - end +def run_without_aborting(tasks) + errors = [] - namespace :integration do - task(:env) do - ENV['AJADAPTER'] = adapter - ENV['AJ_INTEGRATION_TESTS'] = "1" - end + tasks.each do |task| + begin + Rake::Task[task].invoke + rescue Exception + errors << task end end - - desc "Run #{adapter} tests" - task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"] - - desc "Run #{adapter} tests in isolation" - task "isolated_test_#{adapter}" => ["#{adapter}:isolated:env", "test:isolated:#{adapter}"] - - desc "Run #{adapter} integration tests" - task "integration_test_#{adapter}" => ["#{adapter}:integration:env", "test:integration:#{adapter}"] + abort "Errors running #{errors.join(', ')}" if errors.any? end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index a9be2a8f00..5404ece804 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 1b582f5877..3d4f63b261 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2014 David Heinemeier Hansson +# Copyright (c) 2014-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 175a2f0956..622c37098e 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash' + module ActiveJob # Raised when an exception is raised during job arguments deserialization. # @@ -24,10 +26,16 @@ module ActiveJob extend self TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ] + # Serializes a set of arguments. Whitelisted types are returned + # as-is. Arrays/Hashes are serialized element by element. + # All other types are serialized using GlobalID. def serialize(arguments) arguments.map { |argument| serialize_argument(argument) } end + # Deserializes a set of arguments. Whitelisted types are returned + # as-is. Arrays/Hashes are deserialized element by element. + # All other types are deserialized using GlobalID. def deserialize(arguments) arguments.map { |argument| deserialize_argument(argument) } rescue => e @@ -35,16 +43,28 @@ module ActiveJob end private + GLOBALID_KEY = '_aj_globalid'.freeze + SYMBOL_KEYS_KEY = '_aj_symbol_keys'.freeze + 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 - when GlobalID::Identification - argument.to_global_id.to_s when *TYPE_WHITELIST argument + when GlobalID::Identification + { 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 - Hash[ argument.map { |key, value| [ serialize_hash_key(key), serialize_argument(value) ] } ] + 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 @@ -52,23 +72,72 @@ module ActiveJob def deserialize_argument(argument) case argument + when String + GlobalID::Locator.locate(argument) || argument + when *TYPE_WHITELIST + argument when Array argument.map { |arg| deserialize_argument(arg) } when Hash - Hash[ argument.map { |key, value| [ key, deserialize_argument(value) ] } ].with_indifferent_access - when String, GlobalID - GlobalID::Locator.locate(argument) || argument + if serialized_global_id?(argument) + deserialize_global_id argument + else + deserialize_hash(argument) + end else - argument + raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" end end + def serialized_global_id?(hash) + hash.size == 1 and hash.include?(GLOBALID_KEY) + end + + def deserialize_global_id(hash) + 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) + 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, + 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) case key + when *RESERVED_KEYS + raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}") when String, Symbol key.to_s else - raise SerializationError.new("Unsupported hash key type: #{key.class.name}") + 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 diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index a3bec1f827..fd49b3fda5 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -6,7 +6,52 @@ require 'active_job/execution' require 'active_job/callbacks' require 'active_job/logging' -module ActiveJob +module ActiveJob #:nodoc: + # = Active Job + # + # Active Job objects can be configured to work with different backend + # queuing frameworks. To specify a queue adapter to use: + # + # ActiveJob::Base.queue_adapter = :inline + # + # A list of supported adapters can be found in QueueAdapters. + # + # Active Job objects can be defined by creating a class that inherits + # from the ActiveJob::Base class. The only necessary method to + # implement is the "perform" method. + # + # To define an Active Job object: + # + # class ProcessPhotoJob < ActiveJob::Base + # def perform(photo) + # photo.watermark!('Rails') + # photo.rotate!(90.degrees) + # photo.resize_to_fit!(300, 300) + # photo.upload! + # end + # end + # + # Records that are passed in are serialized/deserialized using Global + # ID. More information can be found in Arguments. + # + # To enqueue a job to be performed as soon the queueing system is free: + # + # ProcessPhotoJob.perform_later(photo) + # + # To enqueue a job to be processed at some point in the future: + # + # ProcessPhotoJob.set(wait_until: Date.tomorrow.noon).perform_later(photo) + # + # More information can be found in ActiveJob::Core::ClassMethods#set + # + # A job can also be processed immediately without sending to the queue: + # + # ProcessPhotoJob.perform_now(photo) + # + # == Exceptions + # + # * DeserializationError - Error class for deserialization errors. + # * SerializationError - Error class for serialization errors. class Base include Core include QueueAdapter diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index cafa3438c0..2b6149e84e 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -3,8 +3,8 @@ require 'active_support/callbacks' module ActiveJob # = Active Job Callbacks # - # Active Job provides hooks during the lifecycle of a job. Callbacks allow you - # to trigger logic during the lifecycle of a job. Available callbacks are: + # Active Job provides hooks during the life cycle of a job. Callbacks allow you + # to trigger logic during the life cycle of a job. Available callbacks are: # # * <tt>before_enqueue</tt> # * <tt>around_enqueue</tt> @@ -22,6 +22,8 @@ module ActiveJob define_callbacks :enqueue end + # These methods will be included into any Active Job object, adding + # callbacks for +perform+ and +enqueue+ methods. module ClassMethods # Defines a callback that will get called right before the # job's perform method is executed. @@ -36,7 +38,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def before_perform(*filters, &blk) set_callback(:perform, :before, *filters, &blk) @@ -55,7 +57,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def after_perform(*filters, &blk) set_callback(:perform, :after, *filters, &blk) @@ -75,7 +77,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def around_perform(*filters, &blk) set_callback(:perform, :around, *filters, &blk) @@ -94,7 +96,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def before_enqueue(*filters, &blk) set_callback(:enqueue, :before, *filters, &blk) @@ -113,7 +115,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def after_enqueue(*filters, &blk) set_callback(:enqueue, :after, *filters, &blk) @@ -134,7 +136,7 @@ module ActiveJob # def perform(video_id) # Video.find(video_id).process # end - # end + # end # def around_enqueue(*filters, &blk) set_callback(:enqueue, :around, *filters, &blk) diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index b6dd03a0bc..ddd7d1361c 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -13,17 +13,17 @@ module ActiveJob # Job Identifier attr_accessor :job_id - # Queue on which the job should be run on. + # Queue in which the job will reside. attr_writer :queue_name end + # These methods will be included into any Active Job object, adding + # helpers for de/serialization and creation of job instances. module ClassMethods # Creates a new job instance from a hash created with +serialize+ def deserialize(job_data) - job = job_data['job_class'].constantize.new - job.job_id = job_data['job_id'] - job.queue_name = job_data['queue_name'] - job.serialized_arguments = job_data['arguments'] + job = job_data['job_class'].constantize.new + job.deserialize(job_data) job end @@ -40,16 +40,16 @@ module ActiveJob # # VideoJob.set(queue: :some_queue).perform_later(Video.last) # VideoJob.set(wait: 5.minutes).perform_later(Video.last) - # VideoJob.set(wait_until: Time.tomorroe).perform_later(Video.last) + # VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last) # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last) - # VideoJob.set(queue: :some_queue, wait_until: Time.tomorroe).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last) def set(options={}) ConfiguredJob.new(self, options) end end - # Creates a new job instance. Takes as arguments the arguments that - # will be passed to the perform method. + # Creates a new job instance. Takes the arguments that will be + # passed to the perform method. def initialize(*arguments) @arguments = arguments @job_id = SecureRandom.uuid @@ -67,6 +67,32 @@ module ActiveJob } end + # Attaches the stored job data to the current instance. Receives a hash + # returned from +serialize+ + # + # ==== Examples + # + # 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 + def deserialize(job_data) + self.job_id = job_data['job_id'] + self.queue_name = job_data['queue_name'] + self.serialized_arguments = job_data['arguments'] + end + private def deserialize_arguments_if_needed if defined?(@serialized_arguments) && @serialized_arguments.present? @@ -84,6 +110,3 @@ module ActiveJob end end end - - - diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index e8bc44cbc4..430c17e1bf 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -4,13 +4,14 @@ module ActiveJob module Enqueuing extend ActiveSupport::Concern + # Includes the +perform_later+ method for job initialization. module ClassMethods - # Push a job onto the queue. The arguments must be legal JSON types + # Push a job onto the queue. The arguments must be legal JSON types # (string, int, float, nil, true, false, hash or array) or - # GlobalID::Identification instances. Arbitrary Ruby objects + # GlobalID::Identification instances. Arbitrary Ruby objects # are not supported. # - # Returns an instance of the job class queued with args available in + # Returns an instance of the job class queued with arguments available in # Job#arguments. def perform_later(*args) job_or_instantiate(*args).enqueue @@ -22,7 +23,7 @@ module ActiveJob end end - # Reschedule the job to be re-executed. This is usefull in combination + # Reschedules the job to be re-executed. This is useful in combination # with the +rescue_from+ option. When you rescue an exception from your job # you can ask Active Job to retry performing your job. # @@ -37,6 +38,7 @@ module ActiveJob # rescue_from(ErrorLoadingSite) do # retry_job queue: :low_priority # end + # # def perform(*args) # # raise ErrorLoadingSite if cannot scrape # end @@ -45,7 +47,7 @@ module ActiveJob enqueue options end - # Equeue the job to be performed by the queue adapter. + # Enqueues the job to be performed by the queue adapter. # # ==== Options # * <tt>:wait</tt> - Enqueues the job with the specified delay diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb index d6d67c46e3..79d232da4a 100644 --- a/activejob/lib/active_job/execution.rb +++ b/activejob/lib/active_job/execution.rb @@ -6,6 +6,7 @@ module ActiveJob extend ActiveSupport::Concern include ActiveSupport::Rescuable + # Includes methods for executing and performing jobs instantly. module ClassMethods # Performs the job immediately. # @@ -22,7 +23,7 @@ module ActiveJob end # Performs the job immediately. The job is not sent to the queueing adapter - # and will block the execution until it's finished. + # but directly executed by blocking the execution of others until it's finished. # # MyJob.new(*args).perform_now def perform_now diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 1f25d48326..27a5de93f4 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -5,10 +5,10 @@ module ActiveJob end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 962005cd15..cd29e6908e 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -3,7 +3,7 @@ require 'active_support/tagged_logging' require 'active_support/logger' module ActiveJob - module Logging + module Logging #:nodoc: extend ActiveSupport::Concern included do @@ -50,7 +50,7 @@ module ActiveJob logger.formatter.current_tags.include?("ActiveJob") end - class LogSubscriber < ActiveSupport::LogSubscriber + class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc: def enqueue(event) info do job = event.payload[:job] @@ -75,7 +75,7 @@ module ActiveJob def perform(event) info do job = event.payload[:job] - "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms" + "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms" end end @@ -85,7 +85,12 @@ module ActiveJob end def args_info(job) - job.arguments.any? ? " with arguments: #{job.arguments.map(&:inspect).join(", ")}" : "" + if job.arguments.any? + ' with arguments: ' + + job.arguments.map { |arg| arg.try(:to_global_id).try(:to_s) || arg.inspect }.join(', ') + else + '' + end end def scheduled_at(event) diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index fb54aec75e..aa3ebdbc7b 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -2,21 +2,25 @@ require 'active_job/queue_adapters/inline_adapter' require 'active_support/core_ext/string/inflections' module ActiveJob - module QueueAdapter + # The <tt>ActiveJob::QueueAdapter</tt> module is used to load the + # correct adapter. The default queue adapter is the :inline queue. + module QueueAdapter #:nodoc: extend ActiveSupport::Concern + # Includes the setter method for changing the active queue adapter. module ClassMethods mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter } + # Specify the backend queue provider. The default queue adapter + # 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) - when Class - name_or_adapter + else + name_or_adapter if name_or_adapter.respond_to?(:enqueue) end end @@ -26,4 +30,4 @@ module ActiveJob end end end -end
\ No newline at end of file +end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 345b01ef00..4b91c93dbe 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -1,4 +1,39 @@ module ActiveJob + # == Active Job adapters + # + # Active Job has adapters for the following queueing backends: + # + # * {Backburner}[https://github.com/nesquena/backburner] + # * {Delayed Job}[https://github.com/collectiveidea/delayed_job] + # * {Qu}[https://github.com/bkeepers/qu] + # * {Que}[https://github.com/chanks/que] + # * {queue_classic}[https://github.com/QueueClassic/queue_classic] + # * {Resque 1.x}[https://github.com/resque/resque/tree/1-x-stable] + # * {Sidekiq}[http://sidekiq.org] + # * {Sneakers}[https://github.com/jondot/sneakers] + # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch] + # + # === Backends Features + # + # | | Async | Queues | Delayed | Priorities | Timeout | Retries | + # |-------------------|-------|--------|-----------|------------|---------|---------| + # | Backburner | Yes | Yes | Yes | Yes | Job | Global | + # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | + # | Qu | Yes | Yes | No | No | No | Global | + # | Que | Yes | Yes | Yes | Job | No | Job | + # | queue_classic | Yes | Yes | No* | No | No | No | + # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | + # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | + # | Sneakers | Yes | Yes | No | Queue | Queue | No | + # | Sucker Punch | Yes | Yes | No | No | No | No | + # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | + # | Active Job | Yes | Yes | Yes | No | No | No | + # + # 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. + # module QueueAdapters extend ActiveSupport::Autoload diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb index e1b00f1de7..2453d065de 100644 --- a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -2,19 +2,29 @@ require 'backburner' module ActiveJob module QueueAdapters + # == Backburner adapter for Active Job + # + # Backburner is a beanstalkd-powered job queue that can handle a very + # high volume of jobs. You create background jobs and place them on + # multiple work queues to be processed later. Read more about + # Backburner {here}[https://github.com/nesquena/backburner]. + # + # To use Backburner set the queue_adapter config to +:backburner+. + # + # Rails.application.config.active_job.queue_adapter = :backburner class BackburnerAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name end - def enqueue_at(job, timestamp) + 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 end - class JobWrapper + class JobWrapper #:nodoc: class << self def perform(job_data) Base.execute job_data 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 658799edfc..69d9e70de3 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -2,19 +2,35 @@ require 'delayed_job' module ActiveJob module QueueAdapters + # == Delayed Job adapter for Active Job + # + # Delayed::Job (or DJ) encapsulates the common pattern of asynchronously + # executing longer tasks in the background. Although DJ can have many + # storage backends, one of the most used is based on Active Record. + # Read more about Delayed Job {here}[https://github.com/collectiveidea/delayed_job]. + # + # To use Delayed Job, set the queue_adapter config to +:delayed_job+. + # + # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter class << self - def enqueue(job) - JobWrapper.new.delay(queue: job.queue_name).perform(job.serialize) + def enqueue(job) #:nodoc: + Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) end - def enqueue_at(job, timestamp) - JobWrapper.new.delay(queue: job.queue_name, run_at: Time.at(timestamp)).perform(job.serialize) + def enqueue_at(job, timestamp) #:nodoc: + Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) end end - class JobWrapper - def perform(job_data) + class JobWrapper #:nodoc: + attr_accessor :job_data + + def initialize(job_data) + @job_data = job_data + end + + def perform Base.execute(job_data) end end diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index fdefa38d5e..e25d88e723 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -1,13 +1,21 @@ module ActiveJob module QueueAdapters + # == Active Job Inline adapter + # + # When enqueueing jobs with the Inline adapter the job will be executed + # immediately. + # + # To use the Inline set the queue_adapter config to +:inline+. + # + # Rails.application.config.active_job.queue_adapter = :inline class InlineAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: Base.execute(job.serialize) end - def enqueue_at(*) - raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at https://github.com/rails/activejob") + 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 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 f681fd7e8a..30aa5a4670 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -2,20 +2,33 @@ require 'qu' module ActiveJob module QueueAdapters + # == Qu adapter for Active Job + # + # Qu is a Ruby library for queuing and processing background jobs. It is + # heavily inspired by delayed_job and Resque. Qu was created to overcome + # some shortcomings in the existing queuing libraries. + # The advantages of Qu are: Multiple backends (redis, mongo), jobs are + # requeued when worker is killed, resque-like API. + # + # Read more about Qu {here}[https://github.com/bkeepers/qu]. + # + # To use Qu set the queue_adapter config to +:qu+. + # + # Rails.application.config.active_job.queue_adapter = :qu class QuAdapter class << self - def enqueue(job, *args) + 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_at(job, timestamp, *args) + def enqueue_at(job, timestamp, *args) #:nodoc: raise NotImplementedError end end - class JobWrapper < Qu::Job + class JobWrapper < Qu::Job #:nodoc: def initialize(job_data) @job_data = job_data end diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index 51891ab07b..e501fe0368 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -2,18 +2,30 @@ require 'que' module ActiveJob module QueueAdapters + # == Que adapter for Active Job + # + # Que is a high-performance alternative to DelayedJob or QueueClassic that + # improves the reliability of your application by protecting your jobs with + # the same ACID guarantees as the rest of your data. Que is a queue for + # Ruby and PostgreSQL that manages jobs using advisory locks. + # + # Read more about Que {here}[https://github.com/chanks/que]. + # + # To use Que set the queue_adapter config to +:que+. + # + # Rails.application.config.active_job.queue_adapter = :que class QueAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: JobWrapper.enqueue job.serialize, queue: job.queue_name end - def enqueue_at(job, timestamp) + def enqueue_at(job, timestamp) #:nodoc: JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) end end - class JobWrapper < Que::Job + class JobWrapper < Que::Job #:nodoc: def run(job_data) Base.execute job_data end diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb index ddcc868317..34c11a68b2 100644 --- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -2,17 +2,31 @@ require 'queue_classic' module ActiveJob module QueueAdapters + # == queue_classic adapter for Active Job + # + # queue_classic provides a simple interface to a PostgreSQL-backed message + # queue. queue_classic specializes in concurrent locking and minimizing + # database load while providing a simple, intuitive developer experience. + # queue_classic assumes that you are already using PostgreSQL in your + # production environment and that adding another dependency (e.g. redis, + # beanstalkd, 0mq) is undesirable. + # + # Read more about queue_classic {here}[https://github.com/QueueClassic/queue_classic]. + # + # To use queue_classic set the queue_adapter config to +:queue_classic+. + # + # Rails.application.config.active_job.queue_adapter = :queue_classic class QueueClassicAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) end - def enqueue_at(job, timestamp) + 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)`. ' + 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) @@ -20,7 +34,7 @@ module ActiveJob # 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 suclass + # 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) @@ -28,7 +42,7 @@ module ActiveJob end end - class JobWrapper + class JobWrapper #:nodoc: class << self def perform(job_data) Base.execute job_data diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb index affa3bdfbc..88c6b48fef 100644 --- a/activejob/lib/active_job/queue_adapters/resque_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb @@ -14,13 +14,24 @@ end module ActiveJob module QueueAdapters + # == Resque adapter for Active Job + # + # Resque (pronounced like "rescue") is a Redis-backed library for creating + # background jobs, placing those jobs on multiple queues, and processing + # them later. + # + # Read more about Resque {here}[https://github.com/resque/resque]. + # + # To use Resque set the queue_adapter config to +:resque+. + # + # Rails.application.config.active_job.queue_adapter = :resque class ResqueAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: Resque.enqueue_to job.queue_name, JobWrapper, job.serialize end - def enqueue_at(job, timestamp) + 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" @@ -29,7 +40,7 @@ module ActiveJob end end - class JobWrapper + class JobWrapper #:nodoc: class << self def perform(job_data) Base.execute job_data diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index 79926a1e24..21005fc728 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -2,28 +2,38 @@ require 'sidekiq' module ActiveJob module QueueAdapters + # == Sidekiq adapter for Active Job + # + # Simple, efficient background processing for Ruby. Sidekiq uses threads to + # handle many jobs at the same time in the same process. It does not + # require Rails but will integrate tightly with it to make background + # processing dead simple. + # + # Read more about Sidekiq {here}[http://sidekiq.org]. + # + # To use Sidekiq set the queue_adapter config to +:sidekiq+. + # + # Rails.application.config.active_job.queue_adapter = :sidekiq class SidekiqAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: #Sidekiq::Client does not support symbols as keys Sidekiq::Client.push \ 'class' => JobWrapper, 'queue' => job.queue_name, - 'args' => [ job.serialize ], - 'retry' => true + 'args' => [ job.serialize ] end - def enqueue_at(job, timestamp) + def enqueue_at(job, timestamp) #:nodoc: Sidekiq::Client.push \ 'class' => JobWrapper, 'queue' => job.queue_name, 'args' => [ job.serialize ], - 'retry' => true, 'at' => timestamp end end - class JobWrapper + class JobWrapper #:nodoc: include Sidekiq::Worker def perform(job_data) diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index 1ab0a87485..6d60a2f303 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -3,23 +3,35 @@ require 'thread' module ActiveJob module QueueAdapters + # == Sneakers adapter for Active Job + # + # A high-performance RabbitMQ background processing framework for Ruby. + # Sneakers is being used in production for both I/O and CPU intensive + # workloads, and have achieved the goals of high-performance and + # 0-maintenance, as designed. + # + # Read more about Sneakers {here}[https://github.com/jondot/sneakers]. + # + # To use Sneakers set the queue_adapter config to +:sneakers+. + # + # Rails.application.config.active_job.queue_adapter = :sneakers class SneakersAdapter @monitor = Monitor.new class << self - def enqueue(job) + 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) + def enqueue_at(job, timestamp) #:nodoc: raise NotImplementedError end end - class JobWrapper + class JobWrapper #:nodoc: include Sneakers::Worker from_queue 'default' 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 b19a38093f..be9e7fd03a 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -2,18 +2,33 @@ require 'sucker_punch' module ActiveJob module QueueAdapters + # == Sucker Punch adapter for Active Job + # + # Sucker Punch is a single-process Ruby asynchronous processing library. + # It's girl_friday and DSL sugar on top of Celluloid. With Celluloid's + # actor pattern, we can do asynchronous processing within a single process. + # This reduces costs of hosting on a service like Heroku along with the + # memory footprint of having to maintain additional jobs if hosting on + # a dedicated server. All queues can run within a single Rails/Sinatra + # process. + # + # Read more about Sucker Punch {here}[https://github.com/brandonhilkert/sucker_punch]. + # + # To use Sucker Punch set the queue_adapter config to +:sucker_punch+. + # + # Rails.application.config.active_job.queue_adapter = :sucker_punch class SuckerPunchAdapter class << self - def enqueue(job) + def enqueue(job) #:nodoc: JobWrapper.new.async.perform job.serialize end - def enqueue_at(job, timestamp) + def enqueue_at(job, timestamp) #:nodoc: raise NotImplementedError end end - class JobWrapper + class JobWrapper #:nodoc: include SuckerPunch::Job def perform(job_data) diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index b9997efddf..fd7c0b207a 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -1,76 +1,62 @@ module ActiveJob module QueueAdapters + # == Test adapter for Active Job + # + # The test adapter should be used only in testing. Along with + # <tt>ActiveJob::TestCase</tt> and <tt>ActiveJob::TestHelper</tt> + # it makes a great tool to test your Rails application. + # + # To use the test adapter set queue_adapter config to +:test+. + # + # Rails.application.config.active_job.queue_adapter = :test class TestAdapter - attr_accessor(:perform_enqueued_jobs) { false } - attr_accessor(:perform_enqueued_at_jobs) { false } - delegate :name, to: :class + class << self + attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter) + attr_writer(:enqueued_jobs, :performed_jobs) - # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. - def enqueued_jobs - @enqueued_jobs ||= [] - end + # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. + def enqueued_jobs + @enqueued_jobs ||= [] + end - # Allows you to overwrite the default enqueued jobs store from an array to some - # other object. If you just want to clear the store, - # call ActiveJob::QueueAdapters::TestAdapter.enqueued_jobs.clear. - # - # If you place another object here, please make sure it responds to: - # - # * << (message) - # * clear - # * length - # * size - # * and other common Array methods - def enqueued_jobs=(val) - @enqueued_jobs = val - end + # Provides a store of all the performed jobs with the TestAdapter so you can check them. + def performed_jobs + @performed_jobs ||= [] + end - # Provides a store of all the performed jobs with the TestAdapter so you can check them. - def performed_jobs - @performed_jobs ||= [] - end + def enqueue(job) #:nodoc: + return if filtered?(job) - # Allows you to overwrite the default performed jobs store from an array to some - # other object. If you just want to clear the store, - # call ActiveJob::QueueAdapters::TestAdapter.performed_jobs.clear. - # - # If you place another object here, please make sure it responds to: - # - # * << (message) - # * clear - # * length - # * size - # * and other common Array methods - def performed_jobs=(val) - @performed_jobs = val - end + job_data = job_to_hash(job) + enqueue_or_perform(perform_enqueued_jobs, job, job_data) + end + + def enqueue_at(job, timestamp) #:nodoc: + return if filtered?(job) - def enqueue(job) - if perform_enqueued_jobs? - performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name} - job.perform_now - else - enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name} + job_data = job_to_hash(job, at: timestamp) + enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) end - end - def enqueue_at(job, timestamp) - if perform_enqueued_at_jobs? - performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp} - job.perform_now - else - enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp} + private + + def job_to_hash(job, extras = {}) + { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras) end - end - private - def perform_enqueued_jobs? - perform_enqueued_jobs + def enqueue_or_perform(perform, job, job_data) + if perform + performed_jobs << job_data + Base.execute job.serialize + else + enqueued_jobs << job_data + end end - def perform_enqueued_at_jobs? - perform_enqueued_at_jobs + def filtered?(job) + filter && !Array(filter).include?(job.class) end + end end end end diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 45acb71605..9ae0345120 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -2,10 +2,20 @@ module ActiveJob module QueueName extend ActiveSupport::Concern + # Includes the ability to override the default queue name and prefix. module ClassMethods mattr_accessor(:queue_name_prefix) mattr_accessor(:default_queue_name) { "default" } + # Specifies the name of the queue to process the job on. + # + # class PublishToFeedJob < ActiveJob::Base + # queue_as :feeds + # + # def perform(post) + # post.to_feed! + # end + # end def queue_as(part_name=nil, &block) if block_given? self.queue_name = block @@ -15,15 +25,18 @@ module ActiveJob end def queue_name_from_part(part_name) #:nodoc: - queue_name = part_name.to_s.presence || default_queue_name + queue_name = part_name || default_queue_name name_parts = [queue_name_prefix.presence, queue_name] - name_parts.compact.join('_') + name_parts.compact.join(queue_name_delimiter) end end included do class_attribute :queue_name, instance_accessor: false + class_attribute :queue_name_delimiter, instance_accessor: false + self.queue_name = default_queue_name + self.queue_name_delimiter = '_' # set default delimiter to '_' end # Returns the name of the queue the job will be run on diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index fa0576669e..66508114d1 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActiveJob # Provides helper methods for testing Active Job module TestHelper @@ -5,10 +7,12 @@ module ActiveJob included do def before_setup - @old_queue_adapter = queue_adapter + @old_queue_adapter = queue_adapter ActiveJob::Base.queue_adapter = :test clear_enqueued_jobs clear_performed_jobs + queue_adapter.perform_enqueued_jobs = false + queue_adapter.perform_enqueued_at_jobs = false super end @@ -40,20 +44,28 @@ 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 original_count + number, new_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 - # Assert that no job have been enqueued. + # Asserts that no jobs have been enqueued. # # def test_jobs # assert_no_enqueued_jobs @@ -69,21 +81,37 @@ 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 - def assert_no_enqueued_jobs(&block) - assert_enqueued_jobs 0, &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. + # If no block is passed, <tt>perform_enqueued_jobs</tt> + # must be called around the job call. # # def test_jobs # assert_performed_jobs 0 - # HelloJob.perform_later('xavier') + # + # perform_enqueued_jobs do + # HelloJob.perform_later('xavier') + # end # assert_performed_jobs 1 - # HelloJob.perform_later('yves') - # assert_performed_jobs 2 + # + # perform_enqueued_jobs do + # HelloJob.perform_later('yves') + # assert_performed_jobs 2 + # end # end # # If a block is passed, that block should cause the specified number of @@ -99,10 +127,32 @@ 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 - yield + perform_enqueued_jobs(only: only) { yield } new_count = performed_jobs.size assert_equal original_count + number, new_count, "#{number} jobs expected, but #{new_count - original_count} were performed" @@ -116,8 +166,11 @@ module ActiveJob # # def test_jobs # assert_no_performed_jobs - # HelloJob.perform_later('matthew') - # assert_performed_jobs 1 + # + # perform_enqueued_jobs do + # HelloJob.perform_later('matthew') + # assert_performed_jobs 1 + # end # end # # If a block is passed, that block should not cause any job to be performed. @@ -128,16 +181,38 @@ 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 - def assert_no_performed_jobs(&block) - assert_performed_jobs 0, &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. # - # def assert_enqueued_job + # def test_assert_enqueued_with # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do # MyJob.perform_later(1,2,3) # end @@ -146,11 +221,12 @@ 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 + assert matching_job, "No enqueued job found with #{args}" ensure queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs end @@ -166,15 +242,28 @@ module ActiveJob original_performed_jobs = performed_jobs.dup clear_performed_jobs args.assert_valid_keys(:job, :args, :at, :queue) - yield + 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(only: nil) + @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 + 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 + end + def queue_adapter ActiveJob::Base.queue_adapter end @@ -191,6 +280,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/test/adapters/test.rb b/activejob/test/adapters/test.rb new file mode 100644 index 0000000000..7180b38a57 --- /dev/null +++ b/activejob/test/adapters/test.rb @@ -0,0 +1,3 @@ +ActiveJob::Base.queue_adapter = :test +ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true +ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb index 4fc235ae40..f0c710f9ed 100644 --- a/activejob/test/cases/adapter_test.rb +++ b/activejob/test/cases/adapter_test.rb @@ -1,8 +1,7 @@ require 'helper' class AdapterTest < ActiveSupport::TestCase - test "should load #{ENV['AJADAPTER']} adapter" do - ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym - assert_equal ActiveJob::Base.queue_adapter, "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize + test "should load #{ENV['AJ_ADAPTER']} adapter" do + assert_equal "active_job/queue_adapters/#{ENV['AJ_ADAPTER']}_adapter".classify, ActiveJob::Base.queue_adapter.name end end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb new file mode 100644 index 0000000000..8b9b62190f --- /dev/null +++ b/activejob/test/cases/argument_serialization_test.rb @@ -0,0 +1,103 @@ +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 + @person = Person.find('5') + end + + [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, + 'a', true, false, + [ 1, 'a' ], + { 'a' => 1 } + ].each do |arg| + test "serializes #{arg.class} verbatim" do + assert_arguments_unchanged arg + end + end + + [ :a, Object.new, self, Person.find('5').to_gid ].each do |arg| + test "does not serialize #{arg.class}" do + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [ arg ] + end + + assert_raises ActiveJob::DeserializationError do + ActiveJob::Arguments.deserialize [ arg ] + end + end + end + + test 'should convert records to Global IDs' do + assert_arguments_roundtrip [@person] + end + + test 'should dive deep into arrays and hashes' do + assert_arguments_roundtrip [3, [@person]] + assert_arguments_roundtrip [{ 'a' => @person }] + end + + 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 + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [ { 1 => 2 } ] + end + + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ] + end + + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ] + end + + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ] + end + end + + test 'should not allow non-primitive objects' do + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [Object.new] + end + + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [1, [Object.new]] + 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 + end + + def assert_arguments_roundtrip(args) + assert_equal args, perform_round_trip(args) + end + + 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 9c56ee08b6..64aae00441 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/numeric/time' require 'jobs/hello_job' require 'jobs/logging_job' require 'jobs/nested_job' +require 'models/person' class AdapterTest < ActiveSupport::TestCase include ActiveSupport::LogSubscriber::TestHelper @@ -33,7 +34,7 @@ class AdapterTest < ActiveSupport::TestCase def teardown super ActiveJob::Logging::LogSubscriber.log_subscribers.pop - ActiveJob::Base.logger = @old_logger + set_logger @old_logger end def set_logger(logger) @@ -65,6 +66,14 @@ class AdapterTest < ActiveSupport::TestCase LoggingJob.queue_name = original_queue_name end + def test_globalid_parameter_logging + person = Person.new(123) + LoggingJob.perform_later person + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end + def test_enqueue_job_logging HelloJob.perform_later "Cristian" assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) diff --git a/activejob/test/cases/parameters_test.rb b/activejob/test/cases/parameters_test.rb deleted file mode 100644 index 92f835af5d..0000000000 --- a/activejob/test/cases/parameters_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'helper' -require 'active_job/arguments' -require 'models/person' -require 'active_support/core_ext/hash/indifferent_access' - -class ParameterSerializationTest < ActiveSupport::TestCase - test 'should make no change to regular values' do - assert_equal [ 1, "something" ], ActiveJob::Arguments.serialize([ 1, "something" ]) - end - - test 'should not allow complex objects' do - assert_equal [ nil ], ActiveJob::Arguments.serialize([ nil ]) - assert_equal [ 1 ], ActiveJob::Arguments.serialize([ 1 ]) - assert_equal [ 1.0 ], ActiveJob::Arguments.serialize([ 1.0 ]) - assert_equal [ 'a' ], ActiveJob::Arguments.serialize([ 'a' ]) - assert_equal [ true ], ActiveJob::Arguments.serialize([ true ]) - assert_equal [ false ], ActiveJob::Arguments.serialize([ false ]) - assert_equal [ { "a" => 1, "b" => 2 } ], ActiveJob::Arguments.serialize([ { a: 1, "b" => 2 } ]) - assert_equal [ [ 1 ] ], ActiveJob::Arguments.serialize([ [ 1 ] ]) - assert_equal [ 1_000_000_000_000_000_000_000 ], ActiveJob::Arguments.serialize([ 1_000_000_000_000_000_000_000 ]) - - err = assert_raises ActiveJob::SerializationError do - ActiveJob::Arguments.serialize([ 1, self ]) - end - assert_equal "Unsupported argument type: #{self.class.name}", err.message - end - - test 'should dive deep into arrays or hashes' do - assert_equal [ { "a" => Person.find(5).to_gid.to_s }.with_indifferent_access ], ActiveJob::Arguments.serialize([ { a: Person.find(5) } ]) - assert_equal [ [ Person.find(5).to_gid.to_s ] ], ActiveJob::Arguments.serialize([ [ Person.find(5) ] ]) - end - - test 'should dive deep into arrays or hashes and raise exception on complex objects' do - err = assert_raises ActiveJob::SerializationError do - ActiveJob::Arguments.serialize([ 1, [self] ]) - end - assert_equal "Unsupported argument type: #{self.class.name}", err.message - end - - test 'shoud dive deep into hashes and allow raise exception on not string/symbol keys' do - err = assert_raises ActiveJob::SerializationError do - ActiveJob::Arguments.serialize([ [ { 1 => 2 } ] ]) - end - assert_equal "Unsupported hash key type: Fixnum", err.message - end - - test 'should serialize records with global id' do - assert_equal [ Person.find(5).to_gid.to_s ], ActiveJob::Arguments.serialize([ Person.find(5) ]) - end - - test 'should serialize values and records together' do - assert_equal [ 3, Person.find(5).to_gid.to_s ], ActiveJob::Arguments.serialize([ 3, Person.find(5) ]) - end -end - -class ParameterDeserializationTest < ActiveSupport::TestCase - test 'should make no change to regular values' do - assert_equal [ 1, "something" ], ActiveJob::Arguments.deserialize([ 1, "something" ]) - end - - test 'should deserialize records with global id' do - assert_equal [ Person.find(5) ], ActiveJob::Arguments.deserialize([ Person.find(5).to_gid ]) - end - - test 'should serialize values and records together' do - assert_equal [ 3, Person.find(5) ], ActiveJob::Arguments.deserialize([ 3, Person.find(5).to_gid ]) - end - - test 'should dive deep when deserialising arrays' do - assert_equal [ [ 3, Person.find(5) ] ], ActiveJob::Arguments.deserialize([ [ 3, Person.find(5).to_gid ] ]) - end - - test 'should dive deep when deserialising hashes' do - assert_equal [ { "5" => Person.find(5) } ], ActiveJob::Arguments.deserialize([ { "5" => Person.find(5).to_gid } ]) - end - -end diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb index 4052477543..898016a704 100644 --- a/activejob/test/cases/queue_naming_test.rb +++ b/activejob/test/cases/queue_naming_test.rb @@ -9,8 +9,9 @@ class QueueNamingTest < ActiveSupport::TestCase end test 'uses given queue name job' do + original_queue_name = HelloJob.queue_name + begin - original_queue_name = HelloJob.queue_name HelloJob.queue_as :greetings assert_equal "greetings", HelloJob.new.queue_name ensure @@ -18,9 +19,32 @@ class QueueNamingTest < ActiveSupport::TestCase end end + test 'allows a blank queue name' do + original_queue_name = HelloJob.queue_name + + begin + HelloJob.queue_as "" + assert_equal "", HelloJob.new.queue_name + ensure + HelloJob.queue_name = original_queue_name + end + end + + test 'does not use a nil queue name' do + original_queue_name = HelloJob.queue_name + + begin + HelloJob.queue_as nil + assert_equal "default", HelloJob.new.queue_name + ensure + HelloJob.queue_name = original_queue_name + end + end + test 'evals block given to queue_as to determine queue' do + original_queue_name = HelloJob.queue_name + begin - original_queue_name = HelloJob.queue_name HelloJob.queue_as { :another } assert_equal "another", HelloJob.new.queue_name ensure @@ -29,8 +53,9 @@ class QueueNamingTest < ActiveSupport::TestCase end test 'can use arguments to determine queue_name in queue_as block' do + original_queue_name = HelloJob.queue_name + begin - original_queue_name = HelloJob.queue_name HelloJob.queue_as { self.arguments.first=='1' ? :one : :two } assert_equal "one", HelloJob.new('1').queue_name assert_equal "two", HelloJob.new('3').queue_name @@ -39,11 +64,11 @@ class QueueNamingTest < ActiveSupport::TestCase end end - test 'queu_name_prefix prepended to the queue name' do - begin - original_queue_name_prefix = ActiveJob::Base.queue_name_prefix - original_queue_name = HelloJob.queue_name + test 'queue_name_prefix prepended to the queue name with default delimiter' do + original_queue_name_prefix = ActiveJob::Base.queue_name_prefix + original_queue_name = HelloJob.queue_name + begin ActiveJob::Base.queue_name_prefix = 'aj' HelloJob.queue_as :low assert_equal 'aj_low', HelloJob.queue_name @@ -53,9 +78,25 @@ class QueueNamingTest < ActiveSupport::TestCase end end + test 'queue_name_prefix prepended to the queue name with custom delimiter' do + original_queue_name_prefix = ActiveJob::Base.queue_name_prefix + original_queue_name_delimiter = ActiveJob::Base.queue_name_delimiter + original_queue_name = HelloJob.queue_name + + begin + ActiveJob::Base.queue_name_delimiter = '.' + ActiveJob::Base.queue_name_prefix = 'aj' + HelloJob.queue_as :low + assert_equal 'aj.low', HelloJob.queue_name + ensure + ActiveJob::Base.queue_name_prefix = original_queue_name_prefix + ActiveJob::Base.queue_name_delimiter = original_queue_name_delimiter + HelloJob.queue_name = original_queue_name + end + end + test 'uses queue passed to #set' do job = HelloJob.set(queue: :some_queue).perform_later assert_equal "some_queue", job.queue_name end - end diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb index 1b6c2e9fac..58c9ca8992 100644 --- a/activejob/test/cases/rescue_test.rb +++ b/activejob/test/cases/rescue_test.rb @@ -2,8 +2,6 @@ require 'helper' require 'jobs/rescue_job' require 'models/person' -require 'active_support/core_ext/object/inclusion' - class RescueTest < ActiveSupport::TestCase setup do JobBuffer.clear diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb index 1d0fdbd22d..7d1702990e 100644 --- a/activejob/test/cases/test_case_test.rb +++ b/activejob/test/cases/test_case_test.rb @@ -9,6 +9,6 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase end def test_set_test_adapter - assert_instance_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter + assert_equal 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 eab540bb6c..58de2f2588 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -4,10 +4,10 @@ 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 - setup { queue_adapter.perform_enqueued_at_jobs = true } - def test_assert_enqueued_jobs assert_nothing_raised do assert_enqueued_jobs 1 do @@ -44,11 +44,16 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_no_enqueued_jobs_with_no_block + assert_nothing_raised do + assert_no_enqueued_jobs + end + end + def test_assert_no_enqueued_jobs assert_nothing_raised do assert_no_enqueued_jobs do - # Scheduled jobs are being performed in this context - HelloJob.set(wait_until: Date.tomorrow.noon).perform_later('godfrey') + HelloJob.perform_now end end end @@ -84,9 +89,68 @@ 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 - NestedJob.set(wait_until: Date.tomorrow.noon).perform_later + LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later end end @@ -97,11 +161,13 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end - assert_raise ActiveSupport::TestCase::Assertion do + error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_with(job: NestedJob, queue: 'low') do NestedJob.perform_later end end + + assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message end def test_assert_enqueued_job_args @@ -111,11 +177,28 @@ 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 - setup { queue_adapter.perform_enqueued_jobs = true } - def test_assert_performed_jobs assert_nothing_raised do assert_performed_jobs 1 do @@ -141,22 +224,31 @@ class PerformedJobsTest < ActiveJob::TestCase def test_assert_performed_jobs_with_no_block assert_nothing_raised do - HelloJob.perform_later('rafael') + perform_enqueued_jobs do + HelloJob.perform_later('rafael') + end assert_performed_jobs 1 end assert_nothing_raised do - HelloJob.perform_later('aaron') - HelloJob.perform_later('matthew') - assert_performed_jobs 3 + perform_enqueued_jobs do + HelloJob.perform_later('aaron') + HelloJob.perform_later('matthew') + assert_performed_jobs 3 + end + end + end + + def test_assert_no_performed_jobs_with_no_block + assert_nothing_raised do + assert_no_performed_jobs end end def test_assert_no_performed_jobs assert_nothing_raised do assert_no_performed_jobs do - # Scheduled jobs are being enqueued in this context - HelloJob.set(wait_until: Date.tomorrow.noon).perform_later('godfrey') + # empty block won't perform jobs end end end @@ -192,6 +284,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 @@ -211,4 +380,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 ce22833b11..72ec2b8904 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -5,18 +5,7 @@ require 'support/job_buffer' GlobalID.app = 'aj' -@adapter = ENV['AJADAPTER'] || 'inline' - -def sidekiq? - @adapter == 'sidekiq' -end - -def ruby_193? - RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java' -end - -# Sidekiq doesn't work with MRI 1.9.3 -exit if sidekiq? && ruby_193? +@adapter = ENV['AJ_ADAPTER'] || 'inline' if ENV['AJ_INTEGRATION_TESTS'] require 'support/integration/helper' @@ -26,5 +15,4 @@ end require 'active_support/testing/autorun' -ActiveJob::Base.logger.level = Logger::DEBUG ActiveSupport::TestCase.test_order = :random diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 779dedb53f..38874b51a8 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -3,16 +3,17 @@ require 'jobs/logging_job' require 'active_support/core_ext/numeric/time' class QueuingTest < ActiveSupport::TestCase - test 'should run jobs enqueued on a listenting queue' do + test 'should run jobs enqueued on a listening queue' do TestJob.perform_later @id wait_for_jobs_to_finish_for(5.seconds) assert job_executed end - test 'should not run jobs queued on a non-listenting queue' do + test 'should not run jobs queued on a non-listening queue' do + skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) + old_queue = TestJob.queue_name + begin - skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) - old_queue = TestJob.queue_name TestJob.queue_as :some_other_queue TestJob.perform_later @id wait_for_jobs_to_finish_for(2.seconds) diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb index 056dd073e8..891ed9464e 100644 --- a/activejob/test/jobs/callback_job.rb +++ b/activejob/test/jobs/callback_job.rb @@ -1,12 +1,21 @@ class CallbackJob < ActiveJob::Base before_perform ->(job) { job.history << "CallbackJob ran before_perform" } - after_perform ->(job) { job.history << "CallbackJob ran after_perform" } + after_perform ->(job) { job.history << "CallbackJob ran after_perform" } before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" } - after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" } + after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" } - around_perform :around_perform - around_enqueue :around_enqueue + around_perform do |job, block| + job.history << "CallbackJob ran around_perform_start" + block.call + job.history << "CallbackJob ran around_perform_stop" + end + + around_enqueue do |job, block| + job.history << "CallbackJob ran around_enqueue_start" + block.call + job.history << "CallbackJob ran around_enqueue_stop" + end def perform(person = "david") @@ -17,16 +26,4 @@ class CallbackJob < ActiveJob::Base @history ||= [] end - # FIXME: Not sure why these can't be declared inline like before/after - def around_perform - history << "CallbackJob ran around_perform_start" - yield - history << "CallbackJob ran around_perform_stop" - end - - def around_enqueue - history << "CallbackJob ran around_enqueue_start" - yield - history << "CallbackJob ran around_enqueue_stop" - 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/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb index b50ed36fc2..f80ec3a5a6 100644 --- a/activejob/test/support/delayed_job/delayed/backend/test.rb +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -43,9 +43,7 @@ module Delayed end def self.create(attrs = {}) - new(attrs).tap do |o| - o.save - end + new(attrs).tap(&:save) end def self.create!(*args); create(*args); end diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb index 0cda36a273..2e82562948 100644 --- a/activejob/test/support/integration/adapters/backburner.rb +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -29,7 +29,7 @@ module BackburnerJobsManager def can_run? begin Backburner::Worker.connection.send :connect! - rescue => e + rescue return false end true diff --git a/activejob/test/support/integration/adapters/delayed_job.rb b/activejob/test/support/integration/adapters/delayed_job.rb index dbd0d1a4db..0b591964bc 100644 --- a/activejob/test/support/integration/adapters/delayed_job.rb +++ b/activejob/test/support/integration/adapters/delayed_job.rb @@ -10,7 +10,7 @@ module DelayedJobJobsManager end def start_workers - @worker = Delayed::Worker.new(quiet: false, sleep_delay: 0.5, queues: %w(integration_tests)) + @worker = Delayed::Worker.new(quiet: true, sleep_delay: 0.5, queues: %w(integration_tests)) @thread = Thread.new { @worker.start } end diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb index e913f04a24..3a5b66a057 100644 --- a/activejob/test/support/integration/adapters/qu.rb +++ b/activejob/test/support/integration/adapters/qu.rb @@ -30,7 +30,7 @@ module QuJobsManager def can_run? begin Qu.backend.connection.client.connect - rescue => e + rescue return false end true diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index a5b9b3ec0a..ba7657a42a 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -2,12 +2,12 @@ module QueJobsManager def setup require 'sequel' ActiveJob::Base.queue_adapter = :que - que_url = ENV['QUE_DATABASE_URL'] || 'postgres://localhost/active_jobs_que_int_test' + que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test' uri = URI.parse(que_url) user = uri.user||ENV['USER'] pass = uri.password db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1} + %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} Que.connection = Sequel.connect(que_url) Que.migrate! diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index 81d1935132..038473ccdc 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -1,12 +1,12 @@ module QueueClassicJobsManager def setup - ENV['QC_DATABASE_URL'] ||= 'postgres://localhost/active_jobs_qc_int_test' + ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test' 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 "#{db}"' -U #{user} -t template1} + %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 @@ -17,23 +17,11 @@ module QueueClassicJobsManager def clear_jobs QC::Queue.new("integration_tests").delete_all - retried = false - rescue => e - puts "Got exception while trying to clear jobs: #{e.inspect}" - if retried - puts "Already retried. Raising exception" - raise e - else - puts "Retrying" - retried = true - QC::Conn.connection = QC::Conn.connect - retry - end end def start_workers + QC::Conn.disconnect @pid = fork do - QC::Conn.connection = QC::Conn.connect worker = QC::Worker.new(q_name: 'integration_tests') worker.start end diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb index 03ffd3fd62..912f4bc387 100644 --- a/activejob/test/support/integration/adapters/resque.rb +++ b/activejob/test/support/integration/adapters/resque.rb @@ -1,7 +1,7 @@ module ResqueJobsManager def setup ActiveJob::Base.queue_adapter = :resque - Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "tcp://127.0.0.1:6379/12", :thread_safe => true) + Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "redis://127.0.0.1:6379/12", :thread_safe => true) Resque.logger = Rails.logger unless can_run? puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n" @@ -17,13 +17,15 @@ module ResqueJobsManager def start_workers @resque_thread = Thread.new do - Resque::Worker.new("integration_tests").work(0.5) + w = Resque::Worker.new("integration_tests") + w.term_child = true + w.work(0.5) end @scheduler_thread = Thread.new do Resque::Scheduler.configure do |c| c.poll_sleep_amount = 0.5 c.dynamic = true - c.verbose = true + c.quiet = true c.logfile = nil end Resque::Scheduler.master_lock.release! @@ -39,7 +41,7 @@ module ResqueJobsManager def can_run? begin Resque.redis.client.connect - rescue => e + rescue return false end true diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index b3c3dcff22..6ff18fb56a 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -29,7 +29,6 @@ module SidekiqJobsManager "--concurrency", "1", "--timeout", "1", "--daemon", - "--verbose" ]) require 'celluloid' require 'sidekiq/scheduled' @@ -49,8 +48,9 @@ module SidekiqJobsManager def can_run? begin - Sidekiq.redis { |conn| conn.connect } - rescue => e + Sidekiq.redis(&:info) + Sidekiq.logger = nil + rescue return false end true diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb index f21bb38a32..875803a2d8 100644 --- a/activejob/test/support/integration/adapters/sneakers.rb +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -68,7 +68,7 @@ module SneakersJobsManager def can_run? begin bunny_publisher - rescue => e + rescue return false end true diff --git a/activejob/test/support/integration/adapters/sucker_punch.rb b/activejob/test/support/integration/adapters/sucker_punch.rb index 691ba35c90..9c0d66b469 100644 --- a/activejob/test/support/integration/adapters/sucker_punch.rb +++ b/activejob/test/support/integration/adapters/sucker_punch.rb @@ -1,5 +1,6 @@ module SuckerPunchJobsManager def setup ActiveJob::Base.queue_adapter = :sucker_punch + SuckerPunch.logger = nil end end diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 28aae0f884..09a68738ad 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -1,5 +1,5 @@ -if ENV['AJADAPTER'] == 'delayed_job' - generate "delayed_job:active_record" +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 ccd5036b36..8c2e5a86c2 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -1,6 +1,4 @@ -puts "\n\n" -puts "*** Running integration tests for #{ENV['AJADAPTER']} ***" -puts "\n\n" +puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" ENV["RAILS_ENV"] = "test" ActiveJob::Base.queue_name_prefix = nil @@ -10,7 +8,7 @@ require 'rails/generators/rails/app/app_generator' dummy_app_path = Dir.mktmpdir + "/dummy" dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__) args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle", - "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quite", + "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quiet", "--template", dummy_app_template]).prepare! Rails::Generators::AppGenerator.start args @@ -22,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/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index c14b0688c7..80b42859e2 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,58 +1,84 @@ -* Passwords with spaces only allowed in `ActiveModel::SecurePassword`. +* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` + with no replacement. - Presence validation can be used to restore old behavior. + *Wojciech Wnętrzak* - *Yevhene Shemet* +* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and + `ActiveModel::Errors#[]=` methods that have inconsistent behaviour. -* Validate options passed to `ActiveModel::Validations.validate`. + *Wojciech Wnętrzak* - Preventing, in many cases, the simple mistake of using `validate` instead of `validates`. +* Allow symbol as values for `tokenize` of `LengthValidator` - *Sonny Michaud* + *Kensuke Naito* -* Deprecate `reset_#{attribute}` in favor of `restore_#{attribute}`. +* Assigning an unknown attribute key to an `ActiveModel` instance during initialization + will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of + `NoMethodError`. - These methods may cause confusion with the `reset_changes` that behaves differently - of them. + Example: -* Deprecate `ActiveModel::Dirty#reset_changes` in favor of `#clear_changes_information`. + User.new(foo: 'some value') + # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User. - This method name is causing confusion with the `reset_#{attribute}` - methods. While `reset_name` set the value of the name attribute for the - previous value `reset_changes` only discard the changes and previous - changes. + *Eugene Gilburg* -* Added `restore_attributes` method to `ActiveModel::Dirty` API to restore all the - changed values to the previous data. +* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` + allowing to use it for any object as an includable module. - *Igor G.* + Example: -* Allow proc and symbol as values for `only_integer` of `NumericalityValidator` + class Cat + include ActiveModel::AttributeAssignment + attr_accessor :name, :status + end - *Robin Mehner* + 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' -* `has_secure_password` now verifies that the given password is less than 72 - characters if validations are enabled. + *Bogdan Gusiev* - Fixes #14591. +* Add `ActiveModel::Errors#details` - *Akshay Vishnoi* + To be able to return type of used validator, one can now call `details` + on errors instance. -* Remove deprecated `Validator#setup` without replacement. + Example: - See #10716. + class User < ActiveRecord::Base + validates :name, presence: true + end - *Kuldeep Aggarwal* + user = User.new; user.valid?; user.errors.details + => {name: [{error: :blank}]} -* Add plural and singular form for length validator's default messages. + *Wojciech Wnętrzak* - *Abd ar-Rahman Hamid* +* Change validates_acceptance_of to accept true by default. -* Introduce `validate` as an alias for `valid?`. + 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. - This is more intuitive when you want to run validations but don't care about - the return value. +* Remove deprecated `ActiveModel::Dirty#reset_#{attribute}` and + `ActiveModel::Dirty#reset_changes`. - *Henrik Nyh* + *Rafael Mendonça França* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) for previous changes. +* Change the way in which callback chains can be halted. + + The preferred method to halt a callback chain from now on is to explicitly + `throw(:abort)`. + In the past, returning `false` in an ActiveModel or ActiveModel::Validations + `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 + either not work at all or display a deprecation warning. + + +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index d58dd9ed9b..3ec7a617cf 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index f6beff14e1..4920666f27 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 diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 407dda2ec3..c30a559ef5 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -9,6 +9,7 @@ Rake::TestTask.new do |t| t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end namespace :test do diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 36e565f692..3c6eb56296 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index feb3d9371d..58fe08cc11 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -28,6 +28,7 @@ require 'active_model/version' module ActiveModel extend ActiveSupport::Autoload + autoload :AttributeAssignment autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb new file mode 100644 index 0000000000..356421476c --- /dev/null +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -0,0 +1,63 @@ +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 + + # 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 +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index ea07c5c039..96be551264 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -353,14 +353,12 @@ module ActiveModel @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4) end - def attribute_method_matcher(method_name) #:nodoc: + def attribute_method_matchers_matching(method_name) #:nodoc: attribute_method_matchers_cache.compute_if_absent(method_name) do # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix # will match every time. matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) - match = nil - matchers.detect { |method| match = method.match(method_name) } - match + matchers.map { |method| method.match(method_name) }.compact end end @@ -469,8 +467,8 @@ module ActiveModel # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) - match = self.class.send(:attribute_method_matcher, method_name) - match if match && attribute_method?(match.attr_name) + matches = self.class.send(:attribute_method_matchers_matching, method_name) + matches.detect { |match| attribute_method?(match.attr_name) } end def missing_attribute(attr_name, stack) diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index b27a39b787..2cf39b68fb 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -6,7 +6,7 @@ module ActiveModel # Provides an interface for any class to have Active Record like callbacks. # # Like the Active Record methods, the callback chain is aborted as soon as - # one of the methods in the chain returns +false+. + # one of the methods throws +:abort+. # # First, extend ActiveModel::Callbacks from the class you are creating: # @@ -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] @@ -97,10 +97,12 @@ module ActiveModel # # obj is the MyModel instance that the callback is being called on # end # end + # + # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # `!`, `?` or `=`. def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { - terminator: ->(_,result) { result == false }, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 9c9b6f4a77..9de6ea65be 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -22,7 +22,7 @@ module ActiveModel module Conversion extend ActiveSupport::Concern - # If your object is already designed to implement all of the Active Model + # If your object is already designed to implement all of the \Active \Model # you can use the default <tt>:to_model</tt> implementation, which simply # returns +self+. # @@ -33,9 +33,9 @@ module ActiveModel # person = Person.new # person.to_model == person # => true # - # If your model does not act like an Active Model object, then you should + # If your model does not act like an \Active \Model object, then you should # define <tt>:to_model</tt> yourself returning a proxy object that wraps - # your object with Active Model compliant methods. + # your object with \Active \Model compliant methods. def to_model self end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index ca04f48c1c..c03e5fac79 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -12,7 +12,7 @@ module ActiveModel # * <tt>include ActiveModel::Dirty</tt> in your object. # * Call <tt>define_attribute_methods</tt> passing each method you want to # track. - # * Call <tt>attr_name_will_change!</tt> before each change to the tracked + # * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked # attribute. # * Call <tt>changes_applied</tt> after the changes are persisted. # * Call <tt>clear_changes_information</tt> when you want to reset the changes @@ -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,8 +71,8 @@ module ActiveModel # Save the changes: # # person.save - # person.changed? # => false - # person.name_changed? # => false + # person.changed? # => false + # person.name_changed? # => false # # Reset the changes: # @@ -84,42 +84,41 @@ module ActiveModel # # 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. - # Otherwise Active Model can't track changes to in-place attributes. Note + # 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. # # 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_affix prefix: 'reset_', suffix: '!' 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' @@ -167,15 +166,15 @@ 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 = changed_attributes.include?(attr) + result = changes_include?(attr) result &&= options[:to] == __send__(attr) if options.key?(:to) result &&= options[:from] == changed_attributes[attr] if options.key?(:from) 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 @@ -187,29 +186,30 @@ 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? + # Removes current changes and makes them accessible through +previous_changes+. def changes_applied # :doc: @previously_changed = changes @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 - def reset_changes - ActiveSupport::Deprecation.warn "#reset_changes is deprecated and will be removed on Rails 5. Please use #clear_changes_information instead." - clear_changes_information - 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>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) @@ -222,14 +222,7 @@ module ActiveModel set_attribute_was(attr, value) end - # Handle <tt>reset_*!</tt> for +method_missing+. - def reset_attribute!(attr) - ActiveSupport::Deprecation.warn "#reset_#{attr}! is deprecated and will be removed on Rails 5. Please use #restore_#{attr}! instead." - - restore_attribute!(attr) - 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]) @@ -238,7 +231,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 @@ -247,7 +240,7 @@ module ActiveModel end # Remove changes information for the provided attributes. - def clear_attribute_changes(attributes) + def clear_attribute_changes(attributes) # :doc: attributes_changed_by_setter.except!(*attributes) end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 1b46727351..8334747615 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/deprecation' 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 @@ -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 @@ -98,13 +104,21 @@ module ActiveModel 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 @@ -114,6 +128,12 @@ module ActiveModel # person.errors.set(:name, ["can't be nil"]) # person.errors.get(: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 @@ -121,9 +141,10 @@ module ActiveModel # # person.errors.get(:name) # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => nil + # person.errors.get(:name) # => [] def delete(key) messages.delete(key) + details.delete(key) end # When passed a symbol or a name of a method, returns an array of errors @@ -132,7 +153,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. @@ -140,34 +161,40 @@ 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 @@ -191,8 +218,8 @@ module ActiveModel # 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.add(:name, :blank, message: "can't be blank") + # person.errors.add(:name, :not_specified, message: "must be specified") # person.errors.to_a # => ["name can't be blank", "name must be specified"] def to_a full_messages @@ -200,9 +227,9 @@ module ActiveModel # 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.count # => 1 - # person.errors.add(:name, "must be specified") + # person.errors.add(:name, :not_specified, message: "must be specified") # person.errors.count # => 2 def count to_a.size @@ -221,8 +248,8 @@ module ActiveModel # 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\"?> @@ -259,17 +286,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+). @@ -281,9 +311,9 @@ module ActiveModel # ActiveModel::StrictValidationFailed instead of adding the error. # <tt>:strict</tt> option can also be set to any other exception. # - # person.errors.add(:name, nil, strict: true) + # person.errors.add(:name, :invalid, strict: true) # # => ActiveModel::StrictValidationFailed: name is invalid - # person.errors.add(:name, nil, strict: NameIsInvalid) + # person.errors.add(:name, :invalid, strict: NameIsInvalid) # # => NameIsInvalid: name is invalid # # person.errors.messages # => {} @@ -291,17 +321,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+ @@ -311,6 +347,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 @@ -325,6 +369,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? @@ -337,6 +389,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 @@ -366,7 +419,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. @@ -386,8 +439,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 @@ -445,12 +498,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 diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index dd652190f7..762f4fe939 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -1,14 +1,14 @@ module ActiveModel - # Returns the version of the currently loaded Active Model as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt> def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") 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 4e6b02c246..22010b517c 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' +require 'active_support/core_ext/module/remove_method' module ActiveModel class Name @@ -129,7 +130,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 @@ -189,8 +190,8 @@ module ActiveModel private - def _singularize(string, replacement='_') - ActiveSupport::Inflector.underscore(string).tr('/', replacement) + def _singularize(string) + ActiveSupport::Inflector.underscore(string).tr('/', '_') end end @@ -211,7 +212,7 @@ module ActiveModel # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover" # # Providing the functionality that ActiveModel::Naming provides in your object - # is required to pass the Active Model Lint test. So either extending the + # is required to pass the \Active \Model Lint test. So either extending the # provided method below, or rolling your own is required. module Naming def self.extended(base) #:nodoc: diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index f6ad35769f..871031ece4 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -75,14 +75,7 @@ module ActiveModel end validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED - validates_confirmation_of :password, if: ->{ password.present? } - 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 + validates_confirmation_of :password, allow_blank: true end end end @@ -99,7 +92,7 @@ module ActiveModel # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user def authenticate(unencrypted_password) - BCrypt::Password.new(password_digest) == unencrypted_password && self + BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self end attr_reader :password diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 77f2a64b11..b66dbf1afe 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -130,10 +130,10 @@ module ActiveModel # # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json # person = Person.new - # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true + # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) hash = hash.values.first if include_root diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 3ad3bf30ad..e33c766627 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -6,7 +6,7 @@ require 'active_support/core_ext/time/acts_like' module ActiveModel module Serializers - # == Active Model XML Serializer + # == \Active \Model XML Serializer module Xml extend ActiveSupport::Concern include ActiveModel::Serialization diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 7ee033ba5f..176d4c0607 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -68,8 +68,9 @@ module ActiveModel # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # You can pass a symbol or an array of symbols. - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. @@ -86,6 +87,8 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and # you're looking for more descriptive declaration of your validations. @@ -128,8 +131,9 @@ module ActiveModel # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # You can pass a symbol or an array of symbols. - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, @@ -144,13 +148,17 @@ module ActiveModel options = args.extract_options! if args.all? { |arg| arg.is_a?(Symbol) } - options.assert_valid_keys([:on, :if, :unless]) + options.each_key do |k| + unless VALID_OPTIONS_FOR_VALIDATE.include?(k) + raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?") + end + end end if options.key?(:on) options = options.dup options[:if] = Array(options[:if]) - options[:if].unshift lambda { |o| + options[:if].unshift ->(o) { Array(options[:on]).include?(o.validation_context) } end @@ -363,6 +371,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 @@ -384,9 +401,33 @@ module ActiveModel protected def run_validations! #:nodoc: - run_callbacks :validate + _run_validate_callbacks 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/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 9b5416fb1d..75bf655578 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -1,6 +1,6 @@ module ActiveModel module Validations - # == Active Model Absence Validator + # == \Active \Model Absence Validator class AbsenceValidator < EachValidator #:nodoc: def validate_each(record, attr_name, value) record.errors.add(attr_name, :present, options) if value.present? 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 edfffdd3ce..4b58ef66e3 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -15,15 +15,14 @@ module ActiveModel # after_validation :do_stuff_after_validation # end # - # Like other <tt>before_*</tt> callbacks if +before_validation+ returns - # +false+ then <tt>valid?</tt> will not be called. + # Like other <tt>before_*</tt> callbacks if +before_validation+ throws + # +:abort+ then <tt>valid?</tt> will not be called. module Callbacks extend ActiveSupport::Concern included do include ActiveSupport::Callbacks define_callbacks :validation, - terminator: ->(_,result) { result == false }, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end @@ -58,7 +57,7 @@ module ActiveModel if options.is_a?(Hash) && options[:on] options[:if] = Array(options[:if]) options[:on] = Array(options[:on]) - options[:if].unshift lambda { |o| + options[:if].unshift ->(o) { options[:on].include? o.validation_context } end @@ -98,7 +97,9 @@ module ActiveModel options[:if] = Array(options[:if]) if options[:on] options[:on] = Array(options[:on]) - options[:if].unshift("#{options[:on]}.include? self.validation_context") + options[:if].unshift ->(o) { + options[:on].include? o.validation_context + } end set_callback(:validation, :after, *(args << options), &block) end @@ -108,7 +109,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - run_callbacks(:validation) { super } + _run_validation_callbacks { super } end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index ff3e95da34..46a2e54fba 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -54,7 +54,7 @@ module ActiveModel module HelperMethods # Validates whether the value of the specified attribute is of the correct - # form, going by the regular expression provided.You can require that the + # form, going by the regular expression provided. You can require that the # attribute matches the regular expression: # # class Person < ActiveRecord::Base @@ -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..23201b264a 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -38,7 +38,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 +59,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 @@ -108,10 +112,12 @@ 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. + # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to + # specify how to split up the attribute string. (e.g. + # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method + # or <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/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 5bd162433d..4ba4e3e8f7 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -23,6 +23,10 @@ module ActiveModel raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) raw_value ||= value + if record_attribute_changed_in_place?(record, attr_name) + raw_value = value + end + return if options[:allow_nil] && raw_value.nil? unless value = parse_raw_value_as_a_number(raw_value) @@ -67,7 +71,7 @@ module ActiveModel end def parse_raw_value_as_an_integer(raw_value) - raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ + raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/ end def filtered_options(value) @@ -86,6 +90,13 @@ module ActiveModel options[:only_integer] end end + + private + + def record_attribute_changed_in_place?(record, attr_name) + record.respond_to?(:attribute_changed_in_place?) && + record.attribute_changed_in_place?(attr_name.to_s) + end end module HelperMethods diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index ae8d377fdf..bda436d8d0 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -71,9 +71,11 @@ module ActiveModel # # There is also a list of options that could be used along with validators: # - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index ff41572105..a2531327bf 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -52,8 +52,11 @@ module ActiveModel # end # # Configuration options: - # * <tt>:on</tt> - Specifies when this validation is active - # (<tt>:create</tt> or <tt>:update</tt>). + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 0116de68ab..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 # @@ -127,7 +127,7 @@ module ActiveModel # in the options hash invoking the <tt>validate_each</tt> method passing in the # record, attribute and value. # - # All Active Model validations are built on top of this validator. + # All \Active \Model validations are built on top of this validator. class EachValidator < Validator #:nodoc: attr_reader :attributes @@ -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/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index b1f9082ea7..6da3b4117b 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,7 +1,7 @@ require_relative 'gem_version' module ActiveModel - # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt> def self.version gem_version end diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb new file mode 100644 index 0000000000..402caf21f7 --- /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::AttributeAssignment::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::AttributeAssignment::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/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 5fede098d1..85455c112c 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -7,6 +7,7 @@ class CallbacksTest < ActiveModel::TestCase model.callbacks << :before_around_create yield model.callbacks << :after_around_create + false end end @@ -24,16 +25,22 @@ class CallbacksTest < ActiveModel::TestCase after_create do |model| model.callbacks << :after_create + false end after_create "@callbacks << :final_callback" - def initialize(valid=true) - @callbacks, @valid = [], valid + def initialize(options = {}) + @callbacks = [] + @valid = options[:valid] + @before_create_returns = options.fetch(:before_create_returns, true) + @before_create_throws = options[:before_create_throws] end def before_create @callbacks << :before_create + throw(@before_create_throws) if @before_create_throws + @before_create_returns end def create @@ -51,14 +58,28 @@ class CallbacksTest < ActiveModel::TestCase :after_around_create, :after_create, :final_callback] end - test "after callbacks are always appended" do + test "the callback chain is not halted when around or after callbacks return false" do model = ModelCallbacks.new model.create assert_equal model.callbacks.last, :final_callback end + test "the callback chain is halted when a before callback returns false (deprecated)" do + model = ModelCallbacks.new(before_create_returns: false) + assert_deprecated do + model.create + assert_equal model.callbacks.last, :before_create + end + end + + test "the callback chain is halted when a callback throws :abort" do + model = ModelCallbacks.new(before_create_throws: :abort) + model.create + assert_equal model.callbacks, [:before_create] + end + test "after callbacks are not executed if the block returns false" do - model = ModelCallbacks.new(false) + model = ModelCallbacks.new(valid: false) model.create assert_equal model.callbacks, [ :before_create, :before_around_create, :create, :after_around_create] diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index db2cd885e2..66ed8a350a 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 @@ -181,23 +177,6 @@ class DirtyTest < ActiveModel::TestCase assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes end - test "reset_changes is deprecated" do - @model.name = 'Dmitry' - @model.name_changed? - @model.save - @model.name = 'Bob' - - assert_equal [nil, 'Dmitry'], @model.previous_changes['name'] - assert_equal 'Dmitry', @model.changed_attributes['name'] - - assert_deprecated do - @model.deprecated_reload - end - - assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes - assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes - end - test "restore_attributes should restore all previous data" do @model.name = 'Dmitry' @model.color = 'Red' diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 42d0365521..da142ea2c0 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -29,28 +29,28 @@ class ErrorsTest < ActiveModel::TestCase 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 @@ -59,6 +59,17 @@ class ErrorsTest < ActiveModel::TestCase assert_equal false, errors.has_key?(:name), 'errors should not have key :name' end + def test_key? + errors = ActiveModel::Errors.new(self) + errors[:foo] << 'omg' + assert_equal true, errors.key?(:foo), 'errors should have key :foo' + end + + def test_no_key + errors = ActiveModel::Errors.new(self) + assert_equal false, errors.key?(:name), 'errors should not have key :name' + end + test "clear errors" do person = Person.new person.validate! @@ -70,37 +81,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 @@ -122,7 +137,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 @@ -271,45 +288,101 @@ 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 + assert_deprecated do + person.errors.add_on_empty :name + 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] + assert_deprecated do + person.errors.add_on_empty [:name, :age] + 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' + assert_deprecated do + person.errors.add_on_empty :name, message: 'custom' + 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 + assert_deprecated do + person.errors.add_on_empty :name + 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 + assert_deprecated do + person.errors.add_on_blank :name + 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] + assert_deprecated do + person.errors.add_on_blank [:name, :age] + 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' + assert_deprecated do + person.errors.add_on_blank :name, message: 'custom' + 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 "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/model_test.rb b/activemodel/test/cases/model_test.rb index ee0fa26546..9a8d873ec9 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::AttributeAssignment::UnknownAttributeError) do + SimpleModel.new(hello: 'world') + end end end 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/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 6cd0f4ed4d..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 @@ -30,16 +29,34 @@ class DogWithTwoValidators < Dog before_validation { self.history << 'before_validation_marker2' } end -class DogValidatorReturningFalse < Dog +class DogDeprecatedBeforeValidatorReturningFalse < Dog before_validation { false } before_validation { self.history << 'before_validation_marker2' } end +class DogBeforeValidatorThrowingAbort < Dog + before_validation { throw :abort } + before_validation { self.history << 'before_validation_marker2' } +end + +class DogAfterValidatorReturningFalse < Dog + after_validation { false } + after_validation { self.history << 'after_validation_marker' } +end + class DogWithMissingName < Dog before_validation { self.history << 'before_validation_marker' } 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 } @@ -63,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? @@ -81,13 +116,28 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal ['before_validation_marker1', 'before_validation_marker2'], d.history end - def test_further_callbacks_should_not_be_called_if_before_validation_returns_false - d = DogValidatorReturningFalse.new + def test_further_callbacks_should_not_be_called_if_before_validation_throws_abort + d = DogBeforeValidatorThrowingAbort.new output = d.valid? assert_equal [], d.history assert_equal false, output end + def test_deprecated_further_callbacks_should_not_be_called_if_before_validation_returns_false + d = DogDeprecatedBeforeValidatorReturningFalse.new + assert_deprecated do + output = d.valid? + assert_equal [], d.history + assert_equal false, output + end + end + + def test_further_callbacks_should_be_called_if_after_validation_returns_false + d = DogAfterValidatorReturningFalse.new + d.valid? + assert_equal ['after_validation_marker'], d.history + end + def test_validation_test_should_be_done d = DogWithMissingName.new output = 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..70ee7afecc 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' 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..209903898e 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' @@ -331,6 +330,19 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["Your essay must be at least 5 words."], t.errors[:content] end + + def test_validates_length_of_with_symbol + Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", + tokenizer: :my_word_tokenizer + t = Topic.new(content: "this content should be long enough") + assert t.valid? + + t.content = "not long enough" + assert t.invalid? + assert t.errors[:content].any? + assert_equal ["Your essay must be at least 5 words."], t.errors[:content] + end + def test_validates_length_of_for_fixnum Topic.validates_length_of(:approved, is: 4) diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 3834d327ea..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' @@ -59,7 +58,7 @@ class NumericalityValidationTest < ActiveModel::TestCase def test_validates_numericality_of_with_integer_only_and_proc_as_value Topic.send(:define_method, :allow_only_integers?, lambda { false }) - Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? } + Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?) invalid!(NIL + BLANK + JUNK) valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) @@ -130,7 +129,7 @@ class NumericalityValidationTest < ActiveModel::TestCase def test_validates_numericality_with_proc Topic.send(:define_method, :min_approved, lambda { 5 }) - Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new {|topic| topic.min_approved } + Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved) invalid!([3, 4]) valid!([5, 6]) 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 699a872e42..04101f3545 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -1,9 +1,7 @@ -# encoding: utf-8 require 'cases/helper' require 'models/person' require 'models/topic' require 'models/person_with_validator' -require 'validators/email_validator' require 'validators/namespace/email_validator' class ValidatesTest < ActiveModel::TestCase 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..01804032f0 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' diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index d876f73052..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' @@ -167,10 +166,47 @@ class ValidationsTest < ActiveModel::TestCase end def test_invalid_options_to_validate - assert_raises(ArgumentError) do + error = assert_raises(ArgumentError) do # A common mistake -- we meant to call 'validates' Topic.validate :title, presence: true end + message = 'Unknown key: :presence. Valid keys are: :on, :if, :unless, :prepend. Perhaps you meant to call `validates` instead of `validate`?' + assert_equal message, error.message + end + + def test_callback_options_to_validate + klass = Class.new(Topic) do + attr_reader :call_sequence + + def initialize(*) + super + @call_sequence = [] + end + + private + def validator_a + @call_sequence << :a + end + + def validator_b + @call_sequence << :b + end + + def validator_c + @call_sequence << :c + end + end + + assert_nothing_raised do + klass.validate :validator_a, if: ->{ true } + klass.validate :validator_b, prepend: true + klass.validate :validator_c, unless: ->{ true } + end + + t = klass.new + + assert_predicate t, :valid? + assert_equal [:b, :a], t.call_sequence end def test_errors_conversions @@ -280,7 +316,7 @@ class ValidationsTest < ActiveModel::TestCase ActiveModel::Validations::FormatValidator, ActiveModel::Validations::LengthValidator, ActiveModel::Validations::PresenceValidator - ], validators.map { |v| v.class }.sort_by { |c| c.to_s } + ], validators.map(&:class).sort_by(&:to_s) end def test_list_of_validators_will_be_empty_when_empty @@ -315,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 13e8292954..b66db6a754 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,1196 +1,646 @@ -* A `NullRelation` should represent nothing. This fixes a bug where - `Comment.where(post_id: Post.none)` returned a non-empty result. +* Add `SchemaMigration.create_table` support any unicode charsets for MySQL. - Closes #15176. - - *Matthew Draper*, *Yves Senn* - -* Include default column limits in schema.rb. Allows defaults to be changed - in the future without affecting old migrations that assumed old defaults. - - *Jeremy Kemper* - -* MySQL: schema.rb now includes TEXT and BLOB column limits. - - *Jeremy Kemper* - -* MySQL: correct LONGTEXT and LONGBLOB limits from 2GB to their true 4GB. - - *Jeremy Kemper* - -* SQLite3Adapter now checks for views in `table_exists?`. Fixes #14041. - - *Girish Sonawane* - -* Introduce `connection.supports_views?` to check wether the current adapter - has support for SQL views. Connection adapters should define this method. - - *Yves Senn* - -* Allow included modules to override association methods. - - Fixes #16684. - - *Yves Senn* - -* Schema loading rake tasks (like `db:schema:load` and `db:setup`) maintain - the database connection to the current environment. - - Fixes #16757. - - *Joshua Cody*, *Yves Senn* - -* MySQL: set the connection collation along with the charset. - - Sets the connection collation to the database collation configured in - database.yml. Otherwise, `SET NAMES utf8mb4` will use the default - collation for that charset (utf8mb4_general_ci) when you may have chosen - a different collation, like utf8mb4_unicode_ci. - - This only applies to literal string comparisons, not column values, so it - is unlikely to affect you. - - *Jeremy Kemper* - -* `default_sequence_name` from the PostgreSQL adapter returns a `String`. - - *Yves Senn* - -* Fixed a regression where whitespaces were stripped from DISTINCT queries in - PostgreSQL. - - *Agis Anastasopoulos* - - Fixes #16623. - -* Fix has_many :through relation merging failing when dynamic conditions are - passed as a lambda with an arity of one. - - Fixes #16128. - - *Agis Anastasopoulos* - -* Fixed the `Relation#exists?` to work with polymorphic associations. - - Fixes #15821. - - *Kassio Borges* - -* Currently, Active Record will rescue any errors raised within - after_rollback/after_create callbacks and print them to the logs. Next versions of rails - will not rescue those errors anymore, and just bubble them up, as the other callbacks. - - This adds a opt-in flag to enable that behaviour, of not rescuing the errors. - - Example: - - # Do not swallow errors in after_commit/after_rollback callbacks. - config.active_record.raise_in_transactional_callbacks = true - - Fixes #13460. - - *arthurnn* - -* Fixed an issue where custom accessor methods (such as those generated by - `enum`) with the same name as a global method are incorrectly overridden - when subclassing. - - Fixes #16288. - - *Godfrey Chan* - -* `*_was` and `changes` now work correctly for in-place attribute changes as - well. - - *Sean Griffin* - -* Fix regression on after_commit that didnt fire when having nested transactions. - - Fixes #16425. - - *arthurnn* - -* Do not try to write timestamps when a table has no timestamps columns. - - Fixes #8813. - - *Sergey Potapov* - -* `index_exists?` with `:name` option does verify specified columns. - - Example: - - add_index :articles, :title, name: "idx_title" - - # Before: - index_exists? :articles, :title, name: "idx_title" # => `true` - index_exists? :articles, :body, name: "idx_title" # => `true` - - # After: - index_exists? :articles, :title, name: "idx_title" # => `true` - index_exists? :articles, :body, name: "idx_title" # => `false` - - *Yves Senn*, *Matthew Draper* - -* When calling `update_columns` on a record that is not persisted, the error - message now reflects whether that object is a new record or has been - destroyed. - - *Lachlan Sylvester* - -* Define `id_was` to get the previous value of the primary key. - - Currently when we call id_was and we have a custom primary key name - Active Record will return the current value of the primary key. This - make impossible to correctly do an update operation if you change the - id. - - Fixes #16413. + *Ryuta Kamizono* - *Rafael Mendonça França* +* 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. -* Deprecate `DatabaseTasks.load_schema` to act on the current connection. - Use `.load_schema_current` instead. In the future `load_schema` will - require the `configuration` to act on as an argument. + If you absolutely rely on this behavior, consider patching + `disable_referential_integrity`. *Yves Senn* -* Fixed automatic maintaining test schema to properly handle sql structure - schema format. - - Fixes #15394. - - *Wojciech Wnętrzak* +* Restore aborted transaction state when `disable_referential_integrity` fails + due to missing permissions. -* Fix type casting to Decimal from Float with large precision. + *Toby Ovod-Everett*, *Yves Senn* - *Tomohiro Hashidate* +* PostgreSQL, print warning message if `disable_referential_integrity` fails + due to missing permissions. -* Deprecate `Reflection#source_macro` + *Andrey Nering*, *Yves Senn* - `Reflection#source_macro` is no longer needed in Active Record - source so it has been deprecated. Code that used `source_macro` - was removed in #16353. - - *Eileen M. Uchtitelle*, *Aaron Patterson* - -* No verbose backtrace by db:drop when database does not exist. - - Fixes #16295. - - *Kenn Ejima* - -* Add support for Postgresql JSONB. +* Allow `:limit` option for MySQL bigint primary key support. Example: - create_table :posts do |t| - t.jsonb :meta_data + create_table :foos, id: :primary_key, limit: 8 do |t| end - *Philippe Creux*, *Chris Teague* - -* `db:purge` with MySQL respects `Rails.env`. - - *Yves Senn* - -* `change_column_default :table, :column, nil` with PostgreSQL will issue a - `DROP DEFAULT` instead of a `DEFAULT NULL` query. - - Fixes #16261. + # or - *Matthew Draper*, *Yves Senn* - -* Allow to specify a type for the foreign key column in `references` - and `add_reference`. - - Example: - - change_table :vehicle do |t| - t.references :station, type: :uuid + create_table :foos, id: false do |t| + t.primary_key :id, limit: 8 end - *Andrey Novikov*, *Łukasz Sarnacki* + *Ryuta Kamizono* -* `create_join_table` removes a common prefix when generating the join table. - This matches the existing behavior of HABTM associations. +* `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.) - Fixes #13683. + *Josef Šimánek* - *Stefan Kanev* +* Fixed ActiveRecord::Relation#becomes! and changed_attributes issues for type + column. -* Dont swallow errors on compute_type when having a bad alias_method on - a class. + Fixes #17139. - *arthurnn* + *Miklos Fazekas* -* PostgreSQL invalid `uuid` are convert to nil. +* Format the time string according to the precision of the time column. - *Abdelkader Boudih* + *Ryuta Kamizono* -* Restore 4.0 behavior for using serialize attributes with `JSON` as coder. +* Allow `:precision` option for time type columns. - With 4.1.x, `serialize` started returning a string when `JSON` was passed as - the second attribute. It will now return a hash as per previous versions. + *Ryuta Kamizono* - Example: +* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved + during the given block. - class Post < ActiveRecord::Base - serialize :comment, JSON - end + 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 - include ActiveModel::Model - attr_accessor :category, :text + class Comment < ActiveRecord::Base + belongs_to :commentable, polymorphic: true + after_create -> { Notification.create! comment: self, + recipients: commentable.recipients } end - post = Post.create! - post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.") - post.save! - - # 4.0 - post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} - - # 4.1 before - post.comment # => "#<Comment:0x007f80ab48ff98>" + 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 - # 4.1 after - post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} + *Michael Ryan* - When using `JSON` as the coder in `serialize`, Active Record will use the - new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to - `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped - correctly using the `#as_json` hook. +* `:time` option added for `#touch`. - To keep the previous behaviour, supply a custom coder instead - ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)). + Fixes #18905. - Fixes #15594. + *Hyonjee Joo* - *Jenn Cooper* +* Deprecated passing of `start` value to `find_in_batches` and `find_each` + in favour of `begin_at` value. -* Do not use `RENAME INDEX` syntax for MariaDB 10.0. + *Vipul A M* - Fixes #15931. +* Add `foreign_key_exists?` method. - *Jeff Browning* + *Tõnis Simo* -* Calling `#empty?` on a `has_many` association would use the value from the - counter cache if one exists. +* 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 to memory. + This applies to relations (e.g. `User.all`) as well as associations (e.g. `account.users`) - *David Verhasselt* + # Before: -* Fix the schema dump generated for tables without constraints and with - primary key with default value of custom PostgreSQL function result. + users.none? + # SELECT "users".* FROM "users" - Fixes #16111. + users.one? + # SELECT "users".* FROM "users" - *Andrey Novikov* + # After: -* Fix the SQL generated when a `delete_all` is run on an association to not - produce an `IN` statements. + users.none? + # SELECT 1 AS one FROM "users" LIMIT 1 - Before: + users.one? + # SELECT COUNT(*) FROM "users" - UPDATE "categorizations" SET "category_id" = NULL WHERE - "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2) + *Eugene Gilburg* - After: +* Have `enum` perform type casting consistently with the rest of Active + Record, such as `where`. - UPDATE "categorizations" SET "category_id" = NULL WHERE - "categorizations"."category_id" = 1 + *Sean Griffin* - *Eileen M. Uchitelle, Aaron Patterson* +* `scoping` no longer pollutes the current scope of sibling classes when using + STI. e.x. -* Avoid type casting boolean and ActiveSupport::Duration values to numeric - values for string columns. Otherwise, in some database, the string column - values will be coerced to a numeric allowing false or 0.seconds match any - string starting with a non-digit. + StiOne.none.scoping do + StiTwo.all + end - Example: + Fixes #18806. - App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0' + *Sean Griffin* - *Dylan Thacker-Smith* +* `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. -* Add a `:required` option to singular associations, providing a nicer - API for presence validations on associations. + Fixes #18664. - *Sean Griffin* + *Yves Senn* -* Fixed error in `reset_counters` when associations have `select` scope. - (Call to `count` generates invalid SQL.) +* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start` + parameter to specify where to stop batch processing. - *Cade Truitt* + *Vipul A M* -* After a successful `reload`, `new_record?` is always false. +* Fix rounding problem for PostgreSQL timestamp column. - Fixes #12101. + If timestamp column have the precision, it need to format according to + the precision of timestamp column. - *Matthew Draper* + *Ryuta Kamizono* -* PostgreSQL renaming table doesn't attempt to rename non existent sequences. +* Respect the database default charset for `schema_migrations` table. - *Abdelkader Boudih* + The charset of `version` column in `schema_migrations` table is depend + on the database default charset and collation rather than the encoding + of the connection. -* Move 'dependent: :destroy' handling for 'belongs_to' - from 'before_destroy' to 'after_destroy' callback chain + *Ryuta Kamizono* - Fixes #12380. +* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. - *Ivan Antropov* + These are not valid values to merge in a relation so it should warn the users + early. -* Detect in-place modifications on String attributes. + *Rafael Mendonça França* - Before this change user have to mark the attribute as changed to it be persisted - in the database. Now it is not required anymore. +* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying structure file. - Before: + This makes the db:structure tasks consistent with test:load_structure. - user = User.first - user.name << ' Griffin' - user.name_will_change! - user.save - user.reload.name # => "Sean Griffin" + *Dieter Komendera* - After: +* Respect custom primary keys for associations when calling `Relation#where` - user = User.first - user.name << ' Griffin' - user.save - user.reload.name # => "Sean Griffin" + Fixes #18813. *Sean Griffin* -* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record - is invalid. +* Fixed several edge cases which could result in a counter cache updating + twice or not updating at all for `has_many` and `has_many :through`. - *Bogdan Gusiev*, *Marc Schütz* + Fixes #10865. -* Support for adding and removing foreign keys. Foreign keys are now - a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter - and PostgreSQLAdapter. + *Sean Griffin* - Many thanks to *Matthew Higgins* for laying the foundation with his work on - [foreigner](https://github.com/matthuhiggins/foreigner). +* 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. - Example: + 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. - # within your migrations: - add_foreign_key :articles, :authors - remove_foreign_key :articles, :authors + *Chris Sinjakli* - *Yves Senn* +* 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. -* Fix subtle bugs regarding attribute assignment on models with no primary - key. `'id'` will no longer be part of the attributes hash. + This fixes the issue by skipping validations if the parent record is + persisted, not changed, and not marked for destruction. - *Sean Griffin* + Fixes #17621. -* Deprecate automatic counter caches on `has_many :through`. The behavior was - broken and inconsistent. + *Eileen M. Uchitelle, Aaron Patterson* - *Sean Griffin* +* Fix n+1 query problem when eager loading nil associations (fixes #18312) -* `preload` preserves readonly flag for associations. + *Sammy Larbi* - See #15853. +* 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. - *Yves Senn* + *Henrik Nygren* -* Assume numeric types have changed if they were assigned to a value that - would fail numericality validation, regardless of the old value. Previously - this would only occur if the old value was 0. +* Fixed ActiveRecord::Relation#group method when argument is SQL reserved key word: Example: - model = Model.create!(number: 5) - model.number = '5wibble' - model.number_changed? # => true + SplitTest.group(:key).count + Property.group(:value).count - Fixes #14731. + *Bogdan Gusiev* - *Sean Griffin* +* Added the `#or` method on ActiveRecord::Relation, allowing use of the OR + operator to combine WHERE or HAVING clauses. -* `reload` no longer merges with the existing attributes. - The attribute hash is fully replaced. The record is put into the same state - as it would be with `Model.find(model.id)`. + Example: - *Sean Griffin* + Post.where('id = 1').or(Post.where('id = 2')) + # => SELECT * FROM posts WHERE (id = 1) OR (id = 2) -* The object returned from `select_all` must respond to `column_types`. - If this is not the case a `NoMethodError` is raised. + *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki* - *Sean Griffin* +* Don't define autosave association callbacks twice from + `accepts_nested_attributes_for`. -* Detect in-place modifications of PG array types + Fixes #18704. *Sean Griffin* -* Add `bin/rake db:purge` task to empty the current database. - - *Yves Senn* +* Integer types will no longer raise a `RangeError` when assigning an + attribute, but will instead raise when going to the database. -* Deprecate `serialized_attributes` without replacement. + 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* -* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets - are part of the URI structure, not the actual host. - - Fixes #15705. - - *Andy Bakun*, *Aaron Stone* +* 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. -* Ensure both parent IDs are set on join records when both sides of a - through association are new. + Fixes #18580. *Sean Griffin* -* `ActiveRecord::Dirty` now detects in-place changes to mutable values. - Serialized attributes on ActiveRecord models will no longer save when - unchanged. +* Don't remove join dependencies in `Relation#exists?` - Fixes #8328. + Fixes #18632. *Sean Griffin* -* Pluck now works when selecting columns from different tables with the same - name. +* Invalid values assigned to a JSON column are assumed to be `nil`. - Fixes #15649. + Fixes #18629. *Sean Griffin* -* Remove `cache_attributes` and friends. All attributes are cached. +* 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* -* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. - - *Akshay Vishnoi* - -* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as - `Enumerable#find` does. - - Fixes #15382. - - *James Yang* - -* Make timezone aware attributes work with PostgreSQL array columns. - - Fixes #13402. - - *Kuldeep Aggarwal*, *Sean Griffin* - -* `ActiveRecord::SchemaMigration` has no primary key regardless of the - `primary_key_prefix_type` configuration. +* Introduce the `:if_exists` option for `drop_table`. - Fixes #15051. - - *JoseLuis Torres*, *Yves Senn* - -* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`. - - Fixes #15538. + Example: - *Yves Senn* + drop_table(:posts, if_exists: true) -* Baseclass becomes! subclass. + That would execute: - Before this change, a record which changed its STI type, could not be - updated. + DROP TABLE IF EXISTS posts - Fixes #14785. + If the table doesn't exist, `if_exists: false` (the default) raises an + exception whereas `if_exists: true` does nothing. - *Matthew Draper*, *Earl St Sauver*, *Edo Balvers* + *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono* -* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the - `proper_table_name` instance method on `ActiveRecord::Migration` instead. +* Don't run SQL if attribute value is not changed for update_attribute method. - *Akshay Vishnoi* + *Prathamesh Sonpatki* -* Fix regression on eager loading association based on SQL query rather than - existing column. +* `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 - Fixes #15480. + ActiveRecord::Base.time_zone_aware_types = [:datetime] - *Lauro Caetano*, *Carlos Antonio da Silva* + A deprecation warning will be emitted if you have a `:time` column, and have + not explicitly opted out. -* Deprecate returning `nil` from `column_for_attribute` when no column exists. - It will return a null object in Rails 5.0 + Fixes #3145. *Sean Griffin* -* Implemented ActiveRecord::Base#pretty_print to work with PP. +* Tests now run after_commit callbacks. You no longer have to declare + `uses_transaction ‘test name’` to test the results of an after_commit. - *Ethan* + 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`. -* Preserve type when dumping PostgreSQL point, bit, bit varying and money - columns. + *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper* - *Yves Senn* - -* New records remain new after YAML serialization. +* `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* -* PostgreSQL support default values for enum types. Fixes #7814. - - *Yves Senn* - -* PostgreSQL `default_sequence_name` respects schema. Fixes #7516. - - *Yves Senn* - -* Fixed `columns_for_distinct` of postgresql adapter to work correctly - with orders without sort direction modifiers. - - *Nikolay Kondratyev* - -* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719. - - *Yves Senn* - -* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`. - Fixes duplication in combination with `store_accessor`. - - Fixes #15369. - - *Yves Senn* - -* `rake railties:install:migrations` respects the order of railties. - - *Arun Agrawal* - -* Fix redefine a has_and_belongs_to_many inside inherited class - Fixing regression case, where redefining the same has_an_belongs_to_many - definition into a subclass would raise. - - Fixes #14983. - - *arthurnn* - -* Fix has_and_belongs_to_many public reflection. - When defining a has_and_belongs_to_many, internally we convert that to two has_many. - But as `reflections` is a public API, people expect to see the right macro. - - Fixes #14682. - - *arthurnn* - -* Fixed serialization for records with an attribute named `format`. - - Fixes #15188. - - *Godfrey Chan* - -* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` - on a NullRelation should return a Hash. - - *Kuldeep Aggarwal* - -* Fixed serialized fields returning serialized data after being updated with - `update_column`. - - *Simon Hørup Eskildsen* - -* Fixed polymorphic eager loading when using a String as foreign key. - - Fixes #14734. - - *Lauro Caetano* - -* Change belongs_to touch to be consistent with timestamp updates +* `attribute_will_change!` will no longer cause non-persistable attributes to + be sent to the database. - If a model is set up with a belongs_to: touch relationship the parent - record will only be touched if the record was modified. This makes it - consistent with timestamp updating on the record itself. - - *Brock Trappitt* - -* Fixed the inferred table name of a has_and_belongs_to_many auxiliar - table inside a schema. - - Fixes #14824. - - *Eric Chahin* - -* Remove unused `:timestamp` type. Transparently alias it to `:datetime` - in all cases. Fixes inconsistencies when column types are sent outside of - `ActiveRecord`, such as for XML Serialization. + Fixes #18407. *Sean Griffin* -* Fix bug that added `table_name_prefix` and `table_name_suffix` to - extension names in PostgreSQL when migrating. - - *Joao Carlos* - -* The `:index` option in migrations, which previously was only available for - `references`, now works with any column types. - - *Marc Schütz* - -* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`. - - *jnormore* - -* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having` - or `offset`. +* Remove support for the `protected_attributes` gem. - In these cases the generated query ignored them and that caused unintended - records to be deleted. + *Carlos Antonio da Silva*, *Roberto Miranda* - Fixes #11985. +* Fix accessing of fixtures having non-string labels like Fixnum. - *Leandro Facchinetti* + *Prathamesh Sonpatki* -* Floats with limit >= 25 that get turned into doubles in MySQL no longer have - their limit dropped from the schema. - - Fixes #14135. - - *Aaron Nelson* - -* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many - association. - - Fixes #14709. - - *Kassio Borges* - -* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and - strings in column names as equal. - - This fixes a rare case in which more bind values are passed than there are - placeholders for them in the generated SQL statement, which can make PostgreSQL - throw a `StatementInvalid` exception. - - *Nat Budin* - -* Fix `stored_attributes` to correctly merge the details of stored - attributes defined in parent classes. - - Fixes #14672. - - *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy* - -* `change_column_default` allows `[]` as argument to `change_column_default`. - - Fixes #11586. +* Remove deprecated support to preload instance-dependent associations. *Yves Senn* -* Handle `name` and `"char"` column types in the PostgreSQL adapter. - - `name` and `"char"` are special character types used internally by - PostgreSQL and are used by internal system catalogs. These field types - can sometimes show up in structure-sniffing queries that feature internal system - structures or with certain PostgreSQL extensions. - - *J Smith*, *Yves Senn* - -* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and - NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN` - - Before: - - Point.create(value: 1.0/0) - Point.last.value # => 0.0 - - After: - - Point.create(value: 1.0/0) - Point.last.value # => Infinity - - *Innokenty Mikhailov* - -* Allow the PostgreSQL adapter to handle bigserial primary key types again. - - Fixes #10410. - - *Patrick Robertson* - -* Deprecate joining, eager loading and preloading of instance dependent - associations without replacement. These operations happen before instances - are created. The current behavior is unexpected and can result in broken - behavior. - - Fixes #15024. +* Remove deprecated support for PostgreSQL ranges with exclusive lower bounds. *Yves Senn* -* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. - - has_and_belongs_to_many should fall back to using the normal CollectionAssociation's - size calculation if the collection is not cached or loaded. - - Fixes #14913, #14914. - - *Fred Wu* - -* Return a non zero status when running `rake db:migrate:status` and migration table does - not exist. - - *Paul B.* - -* Add support for module-level `table_name_suffix` in models. - - This makes `table_name_suffix` work the same way as `table_name_prefix` when - using namespaced models. - - *Jenner LaFave* - -* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0. - - In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`. - In 4.0 series it is delegated to `Array#join`. - - *Bogdan Gusiev* - -* Log nil binary column values correctly. - - When an object with a binary column is updated with a nil value - in that column, the SQL logger would throw an exception when trying - to log that nil value. This only occurs when updating a record - that already has a non-nil value in that column since an initial nil - value isn't included in the SQL anyway (at least, when dirty checking - is enabled.) The column's new value will now be logged as `<NULL binary data>` - to parallel the existing `<N bytes of binary data>` for non-nil values. - - *James Coleman* - -* Rails will now pass a custom validation context through to autosave associations - in order to validate child associations with the same context. - - Fixes #13854. - - *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* - -* Stringify all variables keys of MySQL connection configuration. - - When `sql_mode` variable for MySQL adapters set in configuration as `String` - was ignored and overwritten by strict mode option. - - Fixes #14895. - - *Paul Nikitochkin* - -* Ensure SQLite3 statements are closed on errors. - - Fixes #13631. - - *Timur Alperovich* - -* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve. - - *Hector Satre* - -* When using a custom `join_table` name on a `habtm`, rails was not saving it - on Reflections. This causes a problem when rails loads fixtures, because it - uses the reflections to set database with fixtures. - - Fixes #14845. - - *Kassio Borges* - -* Reset the cache when modifying a Relation with cached Arel. - Additionally display a warning message to make the user aware. +* Remove deprecation when modifying a relation with cached arel. + This raises an `ImmutableRelation` error instead. *Yves Senn* -* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures - different spellings of timestamps are treated the same. - - Example: - - mytimestamp.simplified_type('timestamp without time zone') - # => :datetime - mytimestamp.simplified_type('timestamp(6) without time zone') - # => also :datetime (previously would be :timestamp) - - See #14513. - - *Jefferson Lai* - -* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions. +* Added `ActiveRecord::SecureToken` in order to encapsulate generation of + unique tokens for attributes in a model using `SecureRandom`. - Fixes #14841. + *Roberto Miranda* - *Lucas Mazza* +* Change the behavior of boolean columns to be closer to Ruby's semantics. -* Fix name collision with `Array#select!` with `Relation#select!`. + Before this change we had a small set of "truthy", and all others are "falsy". - Fixes #14752. + Now, we have a small set of "falsy" values and all others are "truthy" matching + Ruby's semantics. - *Earl St Sauver* - -* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`. - - If a `has_many` association is adjusted using a scope, and another `has_many :through` - uses this association, then the scope adjustment is unexpectedly neglected. - - Fixes #14537. - - *Jan Habermann* - -* `@destroyed` should always be set to `false` when an object is duped. - - *Kuldeep Aggarwal* - -* Fixed has_many association to make it support irregular inflections. - - Fixes #8928. + *Rafael Mendonça França* - *arthurnn*, *Javier Goizueta* +* Deprecate `ActiveRecord::Base.errors_in_transactional_callbacks=`. -* Fixed a problem where count used with a grouping was not returning a Hash. + *Rafael Mendonça França* - Fixes #14721. +* Change transaction callbacks to not swallow errors. - *Eric Chahin* + Before this change any errors raised inside a transaction callback + were getting rescued and printed in the logs. -* `sanitize_sql_like` helper method to escape a string for safe use in an SQL - LIKE statement. + Now these errors are not rescued anymore and just bubble up, as the other callbacks. - Example: + *Rafael Mendonça França* - class Article - def self.search(term) - where("title LIKE ?", sanitize_sql_like(term)) - end - end +* Remove deprecated `sanitize_sql_hash_for_conditions`. - Article.search("20% _reduction_") - # => Query looks like "... title LIKE '20\% \_reduction\_' ..." + *Rafael Mendonça França* - *Rob Gilson*, *Yves Senn* +* Remove deprecated `Reflection#source_macro`. -* Do not quote uuid default value on `change_column`. + *Rafael Mendonça França* - Fixes #14604. +* Remove deprecated `symbolized_base_class` and `symbolized_sti_name`. - *Eric Chahin* + *Rafael Mendonça França* -* The comparison between `Relation` and `CollectionProxy` should be consistent. +* Remove deprecated `ActiveRecord::Base.disable_implicit_join_references=`. - Example: + *Rafael Mendonça França* - author.posts == Post.where(author_id: author.id) - # => true - Post.where(author_id: author.id) == author.posts - # => true +* Remove deprecated access to connection specification using a string accessor. - Fixes #13506. + Now all strings will be handled as a URL. - *Lauro Caetano* + *Rafael Mendonça França* -* Calling `delete_all` on an unloaded `CollectionProxy` no longer - generates an SQL statement containing each id of the collection: +* Change the default `null` value for `timestamps` to `false`. - Before: + *Rafael Mendonça França* - DELETE FROM `model` WHERE `model`.`parent_id` = 1 - AND `model`.`id` IN (1, 2, 3...) +* Return an array of pools from `connection_pools`. - After: + *Rafael Mendonça França* - DELETE FROM `model` WHERE `model`.`parent_id` = 1 +* Return a null column from `column_for_attribute` when no column exists. - *Eileen M. Uchitelle*, *Aaron Patterson* + *Rafael Mendonça França* -* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select` - which created invalid SQL. +* Remove deprecated `serialized_attributes`. - Fixes #13648. + *Rafael Mendonça França* - *Simon Woker* +* Remove deprecated automatic counter caches on `has_many :through`. -* PostgreSQL adapter only warns once for every missing OID per connection. + *Rafael Mendonça França* - Fixes #14275. +* Change the way in which callback chains can be halted. - *Matthew Draper*, *Yves Senn* + 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 + 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 + either not work at all or display a deprecation warning. -* PostgreSQL adapter automatically reloads it's type map when encountering - unknown OIDs. + *claudiob* - Fixes #14678. +* Clear query cache on rollback. - *Matthew Draper*, *Yves Senn* + *Florian Weingarten* -* Fix insertion of records via `has_many :through` association with scope. +* Fixed setting of foreign_key for through associations while building of new record. - Fixes #3548. + Fixes #12698. *Ivan Antropov* -* Auto-generate stable fixture UUIDs on PostgreSQL. +* Improve a dump of the primary key support. If it is not a default primary key, + correctly dump the type and options. - Fixes #11524. + Fixes #14169, #16599. - *Roderick van Domburg* - -* Fixed a problem where an enum would overwrite values of another enum - with the same name in an unrelated class. + *Ryuta Kamizono* - Fixes #14607. +* Format the datetime string according to the precision of the datetime field. - *Evan Whalen* + Incompatible to rounding behavior between MySQL 5.6 and earlier. -* PostgreSQL and SQLite string columns no longer have a default limit of 255. + In 5.5, when you insert `2014-08-17 12:30:00.999999` the fractional part + is ignored. In 5.6, it's rounded to `2014-08-17 12:30:01`: - Fixes #13435, #9153. + http://bugs.mysql.com/bug.php?id=68760 - *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn* + *Ryuta Kamizono* -* Make possible to have an association called `records`. +* Allow precision option for MySQL datetimes. - Fixes #11645. + *Ryuta Kamizono* - *prathamesh-sonpatki* +* Fixed automatic inverse_of for models nested in module. -* `to_sql` on an association now matches the query that is actually executed, where it - could previously have incorrectly accrued additional conditions (e.g. as a result of - a previous query). CollectionProxy now always defers to the association scope's - `arel` method so the (incorrect) inherited one should be entirely concealed. + *Andrew McCloud* - Fixes #14003. +* Change `ActiveRecord::Relation#update` behavior so that it can + be called without passing ids of the records to be updated. - *Jefferson Lai* + This change allows to update multiple records returned by + `ActiveRecord::Relation` with callbacks and validations. -* Block a few default Class methods as scope name. + # Before + # ArgumentError: wrong number of arguments (1 for 2) + Comment.where(group: 'expert').update(body: "Group of Rails Experts") - For instance, this will raise: + # After + # Comments with group expert updated with body "Group of Rails Experts" + Comment.where(group: 'expert').update(body: "Group of Rails Experts") - scope :public, -> { where(status: 1) } + *Prathamesh Sonpatki* - *arthurnn* +* Fix `reaping_frequency` option when the value is a string. -* Fixed error when using `with_options` with lambda. + This usually happens when it is configured using `DATABASE_URL`. - Fixes #9805. + *korbin* - *Lauro Caetano* +* Fix error message when trying to create an associated record and the foreign + key is missing. -* Switch `sqlite3:///` URLs (which were temporarily - deprecated in 4.1) from relative to absolute. + Before this fix the following exception was being raised: - If you still want the previous interpretation, you should replace - `sqlite3:///my/path` with `sqlite3:my/path`. + NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218> - *Matthew Draper* + Now the message is: -* Treat blank UUID values as `nil`. + ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model. - Example: + *Rafael Mendonça França* - Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil> +* 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 + composite primary keys. - *Dmitry Lavrov* + Fixes #18070. -* Enable support for materialized views on PostgreSQL >= 9.3. + *arthurnn* - *Dave Lee* +* `validates_size_of` / `validates_length_of` do not count records, + which are `marked_for_destruction?`. -* The PostgreSQL adapter supports custom domains. Fixes #14305. + Fixes #7247. *Yves Senn* -* PostgreSQL `Column#type` is now determined through the corresponding OID. - The column types stay the same except for enum columns. They no longer have - `nil` as type but `enum`. - - See #7814. - - *Yves Senn* +* Ensure `first!` and friends work on loaded associations. -* Fixed error when specifying a non-empty default value on a PostgreSQL array column. + Fixes #18237. - Fixes #10613. + *Sean Griffin* - *Luke Steensen* +* `eager_load` preserves readonly flag for associations. -* Fixed error where .persisted? throws SystemStackError for an unsaved model with a - custom primary key that didn't save due to validation error. + Closes #15853. - Fixes #14393. + *Takashi Kokubun* - *Chris Finne* +* Provide `:touch` option to `save()` to accommodate saving without updating + timestamps. -* Introduce `validate` as an alias for `valid?`. + Fixes #18202. - This is more intuitive when you want to run validations but don't care about the return value. + *Dan Olson* - *Henrik Nyh* +* Provide a more helpful error message when an unsupported class is passed to + `serialize`. -* Create indexes inline in CREATE TABLE for MySQL. + Fixes #18224. - This is important, because adding an index on a temporary table after it has been created - would commit the transaction. + *Sean Griffin* - It also allows creating and dropping indexed tables with fewer queries and fewer permissions - required. +* Add bigint primary key support for MySQL. Example: - create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t| - t.index :zip + create_table :foos, id: :bigint do |t| end - # => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query - - *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca* - -* Use singular table name in generated migrations when - `ActiveRecord::Base.pluralize_table_names` is `false`. - Fixes #13426. - - *Kuldeep Aggarwal* - -* `touch` accepts many attributes to be touched at once. - - Example: - - # touches :signed_at, :sealed_at, and :updated_at/on attributes. - Photo.last.touch(:signed_at, :sealed_at) + *Ryuta Kamizono* - *James Pinto* +* Support for any type primary key. -* `rake db:structure:dump` only dumps schema information if the schema - migration table exists. + Fixes #14194. - Fixes #14217. + *Ryuta Kamizono* - *Yves Senn* +* Dump the default `nil` for PostgreSQL UUID primary key. -* Reap connections that were checked out by now-dead threads, instead - of waiting until they disconnect by themselves. Before this change, - a suitably constructed series of short-lived threads could starve - the connection pool, without ever having more than a couple alive at - the same time. + *Ryuta Kamizono* - *Matthew Draper* +* Add a `:foreign_key` option to `references` and associated migration + methods. The model and migration generators now use this option, rather than + the `add_foreign_key` form. -* `pk_and_sequence_for` now ensures that only the pg_depend entries - pointing to pg_class, and thus only sequence objects, are considered. + *Sean Griffin* - *Josh Williams* +* Don't raise when writing an attribute with an out-of-range datetime passed + by the user. -* `where.not` adds `references` for `includes` like normal `where` calls do. + *Grey Baker* - Fixes #14406. +* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with + `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. *Yves Senn* -* Extend fixture `$LABEL` replacement to allow string interpolation. +* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to + be marked as having changed when set to the same negative value. - Example: + Closes #18161. - martin: - email: $LABEL@email.com + *Daniel Fox* - users(:martin).email # => martin@email.com +* Introduce `force: :cascade` option for `create_table`. Using this option + will recreate tables even if they have dependent objects (like foreign keys). + `db/schema.rb` now uses `force: :cascade`. This makes it possible to + reload the schema when foreign keys are in place. - *Eric Steele* - -* Add support for `Relation` be passed as parameter on `QueryCache#select_all`. + *Matthew Draper*, *Yves Senn* - Fixes #14361. +* `db:schema:load` and `db:structure:load` no longer purge the database + before loading the schema. This is left for the user to do. + `db:test:prepare` will still purge the database. - *arthurnn* + Closes #17945. -* Passing an Active Record object to `find` or `exists?` is now deprecated. - Call `.id` on the object first. - - *Aaron Patterson* + *Yves Senn* -* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. +* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`. *Ryuta Kamizono* -* Support for MySQL 5.6 fractional seconds. - - *arthurnn*, *Tatsuhiko Miyagawa* - -* Support for Postgres `citext` data type enabling case-insensitive where - values without needing to wrap in UPPER/LOWER sql functions. - - *Troy Kruthoff*, *Lachlan Sylvester* - -* Only save has_one associations if record has changes. - Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one - object did not get saved to the db. - - *Alan Kennedy* +* Add `foreign_type` option to `has_one` and `has_many` association macros. -* Allow strings to specify the `#order` value. + This option enables to define the column name of associated object's type for polymorphic associations. - Example: - - Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql - - *Marcelo Casiraghi*, *Robin Dupret* - -* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID" - warnings on enum columns. - - *Dieter Komendera* - -* `includes` is able to detect the right preloading strategy when string - joins are involved. - - Fixes #14109. - - *Aaron Patterson*, *Yves Senn* - -* Fixed error with validation with enum fields for records where the - value for any enum attribute is always evaluated as 0 during - uniqueness validation. - - Fixes #14172. + *Ulisses Almeida*, *Kassio Borges* - *Vilius Luneckas* *Ahmed AbouElhamayed* +* Remove deprecated behavior allowing nested arrays to be passed as query + values. -* `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` associations *and* on `has_many :through` - associations. Before this change, `before_add` callbacks would be fired - before the record was saved on `has_and_belongs_to_many` associations, but - *not* on `has_many :through` associations. + *Melanie Gilman* - Fixes #14144. +* Deprecate passing a class as a value in a query. Users should pass strings + instead. -* Fixed STI classes not defining an attribute method if there is a - conflicting private method defined on its ancestors. + *Melanie Gilman* - Fixes #11569. +* `add_timestamps` and `remove_timestamps` now properly reversible with + options. - *Godfrey Chan* - -* Coerce strings when reading attributes. Fixes #10485. - - Example: - - book = Book.new(title: 12345) - book.save! - book.title # => "12345" - - *Yves Senn* - -* Deprecate half-baked support for PostgreSQL range values with excluding beginnings. - We currently map PostgreSQL ranges to Ruby ranges. This conversion is not fully - possible because the Ruby range does not support excluded beginnings. - - The current solution of incrementing the beginning is not correct and is now - deprecated. For subtypes where we don't know how to increment (e.g. `#succ` - is not defined) it will raise an ArgumentException for ranges with excluding - beginnings. - - *Yves Senn* + *Noam Gagliardi-Rabinovich* -* Support for user created range types in PostgreSQL. +* `ActiveRecord::ConnectionAdapters::ColumnDumper#column_spec` and + `ActiveRecord::ConnectionAdapters::ColumnDumper#prepare_column_options` no + longer have a `types` argument. They should access + `connection#native_database_types` directly. *Yves Senn* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 2950f05b11..7c2197229d 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 569685bd45..bae40604b1 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -16,15 +16,19 @@ 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: - $ bundle exec rake test_mysql - $ bundle exec rake test_mysql2 - $ bundle exec rake test_postgresql - $ bundle exec rake test_sqlite3 - $ bundle exec rake test_sqlite3_mem + $ bundle exec rake test:mysql + $ 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 File}[rdoc-label:label-Config+File]. (the exact set of available tests is diff --git a/activerecord/Rakefile b/activerecord/Rakefile index b1069e5dcc..976b559da9 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -55,6 +55,7 @@ end t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) } namespace :isolated do diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 6231851be5..c5cd0c89f7 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '>= 6.0.0.beta1', '< 6.1' + s.add_dependency 'arel', '7.0.0.alpha' end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index d3546ce948..a5a1f284a0 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -39,8 +39,8 @@ class Exhibit < ActiveRecord::Base where("notes IS NOT NULL") end - def self.look(exhibits) exhibits.each { |e| e.look } end - def self.feel(exhibits) exhibits.each { |e| e.feel } end + def self.look(exhibits) exhibits.each(&:look) end + def self.feel(exhibits) exhibits.each(&:feel) end end def progress_bar(int); print "." if (int%100).zero? ; end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9028970a3d..ef14e065ae 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -62,10 +62,13 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :Suppressor + autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation autoload :Validations + autoload :SecureToken eager_autoload do autoload :ActiveRecordError, 'active_record/errors' diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e576ec4d40..3d497a30fb 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" @@ -230,8 +242,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) - attrs = mapping.collect {|key, _| read_attribute(key)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| _read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) @@ -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 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 18da28d480..cecb2dbd0b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -46,12 +46,18 @@ module ActiveRecord end end + class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass._reflections.keys - super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -110,6 +116,7 @@ module ActiveRecord 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 :BelongsToAssociation, 'active_record/associations/belongs_to_association' @@ -138,20 +145,14 @@ module ActiveRecord autoload :AliasTracker, 'active_record/associations/alias_tracker' 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 @@ -159,7 +160,31 @@ 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 + # 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 responds to :loaded?, nil otherwise. def association_instance_get(name) @association_cache[name] @@ -993,6 +1018,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>, @@ -1005,7 +1032,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). # @@ -1170,8 +1197,14 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ # association will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] - # Specify the method that returns the primary key used for the association. By default this is +id+. + # Specify the name of the column to use as the primary key for the association. By default this is +id+. # [:dependent] # Controls what happens to the associated objects when # their owner is destroyed. Note that these are implemented as @@ -1232,6 +1265,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" } @@ -1313,10 +1350,18 @@ 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 # will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:as] @@ -1359,7 +1404,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 @@ -1411,7 +1456,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) } # @@ -1475,10 +1520,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" @@ -1487,11 +1536,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 @@ -1694,7 +1743,7 @@ 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].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k| hm_options[k] = options[k] if options.key? k end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index a6a1947148..2b7e4f28c5 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,20 +5,23 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :connection + attr_reader :aliases - def self.empty(connection) - new connection, Hash.new(0) + def self.create(connection, initial_table, type_caster) + aliases = Hash.new(0) + aliases[initial_table] = 1 + new connection, aliases, type_caster end - def self.create(connection, table_joins) - if table_joins.empty? - empty connection + def self.create_with_joins(connection, initial_table, joins, type_caster) + if joins.empty? + create(connection, initial_table, type_caster) else - aliases = Hash.new { |h,k| - h[k] = initial_count_for(connection, k, table_joins) + aliases = Hash.new { |h, k| + h[k] = initial_count_for(connection, k, joins) } - new connection, aliases + aliases[initial_table] = 1 + new connection, aliases, type_caster end end @@ -51,45 +54,37 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases) + def initialize(connection, aliases, type_caster) @aliases = aliases @connection = connection + @type_caster = type_caster end def aliased_table_for(table_name, aliased_name) - table_alias = aliased_name_for(table_name, aliased_name) - - if table_alias == table_name - Arel::Table.new(table_name) - else - Arel::Table.new(table_name).alias(table_alias) - end - end - - def aliased_name_for(table_name, aliased_name) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - table_name + Arel::Table.new(table_name, type_caster: @type_caster) else # Otherwise, we need to use an alias - aliased_name = connection.table_alias_for(aliased_name) + aliased_name = @connection.table_alias_for(aliased_name) # Update the count aliases[aliased_name] += 1 - if aliases[aliased_name] > 1 + table_alias = if aliases[aliased_name] > 1 "#{truncate(aliased_name)}_#{aliases[aliased_name]}" else aliased_name end + Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) end end private def truncate(name) - name.slice(0, connection.table_alias_length - 2) + name.slice(0, @connection.table_alias_length - 2) end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index b965230e60..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, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - 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 - alias_tracker = AliasTracker.empty connection + 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, alias_tracker) + add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) end def join_type @@ -61,132 +49,114 @@ module ActiveRecord binds end - private + protected - def construct_tables(chain, klass, refl, alias_tracker) - chain.map do |reflection| - alias_tracker.aliased_table_for( - table_name_for(reflection, klass, refl), - table_alias_for(reflection, refl, reflection != refl) - ) - end - end - - def table_alias_for(reflection, refl, join = false) - name = "#{reflection.plural_name}_#{alias_suffix(refl)}" - name << "_join" if join - name - end + 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, alias_tracker) - columns = alias_tracker.connection.schema_cache.columns_hash(table_name) - columns[column_name] - end - - def bind_value(scope, column, value, alias_tracker) - @bind_substitution.bind_value scope, column, value, alias_tracker - end - - def bind(scope, table_name, column_name, value, tracker) - column = column_for table_name, column_name, tracker - bind_value scope, column, value, tracker - end - - def last_chain_scope(scope, table, reflection, owner, tracker, assoc_klass) - join_keys = reflection.join_keys(assoc_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], tracker - 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, tracker - 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 next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(assoc_klass) + def transform_value(value) + value_transformation.call(value) + end + + def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key 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, tracker - 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)) end - def add_constraints(scope, owner, assoc_klass, refl, tracker) - chain = refl.chain - scope_chain = refl.scope_chain + class ReflectionProxy < SimpleDelegator # :nodoc: + attr_accessor :next + attr_reader :alias_name - tables = construct_tables(chain, assoc_klass, refl, tracker) + def initialize(reflection, alias_name) + super(reflection) + @alias_name = alias_name + end - owner_reflection = chain.last - table = tables.last - scope = last_chain_scope(scope, table, owner_reflection, owner, tracker, assoc_klass) + def all_includes; nil; end + end - chain.each_with_index do |reflection, i| - table, foreign_table = tables.shift, tables.first + def get_chain(reflection, association, tracker) + name = reflection.name + runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) + previous_reflection = runtime_reflection + reflection.chain.drop(1).each do |refl| + alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + proxy = ReflectionProxy.new(refl, alias_name) + previous_reflection.next = proxy + previous_reflection = proxy + end + [runtime_reflection, previous_reflection] + end - unless reflection == chain.last - next_reflection = chain[i + 1] - scope = next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection) - end + 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, association_klass) - is_first_chain = i == 0 - klass = is_first_chain ? assoc_klass : reflection.klass + reflection = chain_head + loop do + break unless reflection + table = reflection.alias_name + + unless reflection == chain_tail + next_reflection = reflection.next + foreign_table = next_reflection.alias_name + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + end # Exclude the scope of the association itself, because that # was already merged in the #scope method. - scope_chain[i].each do |scope_chain_item| - item = eval_scope(klass, scope_chain_item, owner) + reflection.constraints.each do |scope_chain_item| + 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 - if is_first_chain + 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 + + reflection = reflection.next end scope end - def alias_suffix(refl) - refl.name - end - - def table_name_for(reflection, klass, refl) - if reflection == refl - # If this is a polymorphic belongs_to, we want to get the klass from the - # association because it depends on the polymorphic_type attribute of - # the owner - klass.table_name - else - reflection.table_name - end - end - def eval_scope(klass, scope, owner) klass.unscoped.instance_exec(owner, &scope) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 81fdd681de..265a65c4c1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -68,16 +68,19 @@ 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 # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner[reflection.foreign_key] + record.id != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) end def remove_keys @@ -85,7 +88,7 @@ module ActiveRecord end def foreign_key_present? - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -99,12 +102,13 @@ module ActiveRecord if options[:primary_key] owner.send(reflection.name).try(:id) else - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end end def stale_state - owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s + result = owner._read_attribute(reflection.foreign_key) + result && result.to_s end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 947d61ee7b..88406740d8 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/attribute_accessors' - # This is the parent Association class which defines the variables # used by all associations. # @@ -15,15 +13,10 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self attr_accessor :extensions - # TODO: This class accessor is needed to make activerecord-deprecated_finders work. - # We can move it to a constant in 5.0. - attr_accessor :valid_options end self.extensions = [] - self.valid_options = [:class_name, :class, :foreign_key, :validate] - - attr_reader :name, :scope, :options + VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) @@ -32,57 +25,60 @@ module ActiveRecord::Associations::Builder "Please choose a different association name." end - builder = create_builder model, name, scope, options, &block - reflection = builder.build(model) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension define_accessors model, reflection define_callbacks model, reflection define_validations model, reflection - builder.define_extensions model reflection end - def self.create_builder(model, name, scope, options, &block) + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - new(model, name, scope, options, &block) - end - - def initialize(model, name, scope, options) - # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders. if scope.is_a?(Hash) options = scope scope = nil end - # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders. - @name = name - @scope = scope - @options = options + validate_options(options) - validate_options + scope = build_scope(scope, extension) + + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end + + def self.build_scope(scope, extension) + new_scope = scope if scope && scope.arity == 0 - @scope = proc { instance_exec(&scope) } + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def build(model) - ActiveRecord::Reflection.create(macro, name, scope, options, model) + def self.wrap_scope(scope, extension) + scope end - def macro + def self.macro raise NotImplementedError end - def valid_options - Association.valid_options + Association.extensions.flat_map(&:valid_options) + def self.valid_options(options) + VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def validate_options - options.assert_valid_keys(valid_options) + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def define_extensions(model) + def self.define_extensions(model, name) end def self.define_callbacks(model, reflection) @@ -133,8 +129,6 @@ module ActiveRecord::Associations::Builder raise NotImplementedError end - private - def self.check_dependent_options(dependent) unless valid_dependent_options.include? dependent raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 954ea3878a..ec135d49b7 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,11 +1,11 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options - super + [:foreign_type, :polymorphic, :touch, :counter_cache] + def self.valid_options(options) + super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -23,8 +23,6 @@ module ActiveRecord::Associations::Builder add_counter_cache_methods mixin end - private - def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_update @@ -112,5 +110,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/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index bc15a49996..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -7,22 +7,11 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options + def self.valid_options(options) super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension - - def initialize(model, name, scope, options) - super - @mod = nil - if block_given? - @mod = Module.new(&Proc.new) - @scope = wrap_scope @scope, @mod - end - end - def self.define_callbacks(model, reflection) super name = reflection.name @@ -32,10 +21,11 @@ module ActiveRecord::Associations::Builder } end - def define_extensions(model) - if @mod + def self.define_extensions(model, name) + if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - model.parent.const_set(extension_module_name, @mod) + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) end end @@ -78,9 +68,7 @@ module ActiveRecord::Associations::Builder CODE end - private - - def wrap_scope(scope, mod) + def self.wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else 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 34a555dfd4..93dc4ae118 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 @@ -11,7 +11,7 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") end private @@ -87,18 +87,18 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - hm_builder = HasMany.create_builder(lhs_model, - middle_name, - nil, - middle_options) - hm_builder.build lhs_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) end private def middle_options(join_model) middle_options = {} - middle_options[:class] = join_model + middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" middle_options[:source] = join_model.left_reflection.name if options.key? :foreign_key middle_options[:foreign_key] = options[:foreign_key] @@ -110,7 +110,7 @@ module ActiveRecord::Associations::Builder rhs_options = {} if options.key? :class_name - rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key rhs_options[:class_name] = options[:class_name] end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 4c8c826f76..1c1b47bd56 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,11 +1,11 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] + def self.valid_options(options) + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index c194c8ae9a..a272d3c781 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,11 +1,11 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options - valid = super + [:as] + def self.valid_options(options) + valid = super + [:as, :foreign_type] valid += [:through, :source, :source_type] if options[:through] valid end @@ -14,10 +14,15 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - private - 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 6e6dd7204c..42542f188e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -2,7 +2,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options + def self.valid_options(options) super + [:dependent, :primary_key, :inverse_of, :required] end @@ -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 1836ff0910..82a7c27799 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,7 +33,13 @@ module ActiveRecord reload end - @proxy ||= CollectionProxy.create(klass, self) + if owner.new_record? + # 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) + else + @proxy ||= CollectionProxy.create(klass, self) + end end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -56,9 +62,9 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) pk_type = reflection.primary_key_type - ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_type.type_cast_from_user(i) } - replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) + ids = Array(ids).reject(&:blank?) + ids.map! { |i| pk_type.cast(i) } + replace(klass.find(ids).index_by(&:id).values_at(*ids)) end def reset @@ -123,6 +129,16 @@ module ActiveRecord first_nth_or_last(:last, *args) end + def take + if loaded? + target.first + else + scope.take.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) } @@ -145,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) @@ -169,8 +186,8 @@ module ActiveRecord end # Removes all records from the association without calling callbacks - # on the associated records. It honors the `:dependent` option. However - # if the `:dependent` value is `:destroy` then in that case the `:delete_all` + # on the associated records. It honors the +:dependent+ option. However + # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+ # deletion strategy for the association is applied. # # You can force a particular deletion strategy by passing a parameter. @@ -212,11 +229,7 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, count_options = {}) - # TODO: Remove count_options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, count_options = nil, column_name if column_name.is_a?(Hash) - + def count(column_name = nil) relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -283,7 +296,7 @@ module ActiveRecord elsif !loaded? && !association_scope.group_values.empty? load_target.size elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) - unsaved_records = target.select { |r| r.new_record? } + unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else count_records @@ -316,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) } @@ -326,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) } @@ -352,6 +367,7 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else + replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } end @@ -379,11 +395,18 @@ module ActiveRecord target end - def add_to_target(record, skip_callbacks = false) + def add_to_target(record, skip_callbacks = false, &block) + if association_scope.distinct_value + index = @target.index(record) + end + replace_on_target(record, index, skip_callbacks, &block) + end + + def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks yield(record) if block_given? - if association_scope.distinct_value && index = @target.index(record) + if index @target[index] = record else @target << record @@ -407,7 +430,13 @@ module ActiveRecord private def get_records - return scope.to_a if reflection.scope_chain.any?(&:any?) || scope.eager_loading? + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -486,7 +515,7 @@ module ActiveRecord def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } - existing_records = records.reject { |r| r.new_record? } + existing_records = records.reject(&:new_record?) if existing_records.empty? remove_records(existing_records, records, method) @@ -522,10 +551,18 @@ module ActiveRecord target end + def replace_common_records_in_memory(new_target, original_target) + common_records = new_target & original_target + common_records.each do |record| + skip_callbacks = true + replace_on_target(record, @target.index(record), skip_callbacks) + end + end + 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? @@ -569,8 +606,8 @@ module ActiveRecord if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| - target = source.send(reflection.source_reflection.name) - target.respond_to?(:include?) ? target.include?(record) : target == record + target_reflection = source.send(reflection.source_reflection.name) + target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record } || target.include?(record) else target.include?(record) @@ -581,7 +618,7 @@ module ActiveRecord # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq + ids = args.flatten.compact.map(&:to_s).uniq if ids.size == 1 id = ids.first diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8c7b0b4be9..87e80e88b2 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,10 +29,11 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + delegate :find_nth, to: :scope def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end @@ -226,6 +227,10 @@ module ActiveRecord @association.last(*args) end + def take + @association.take + 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 @@ -355,14 +360,15 @@ module ActiveRecord @association.replace(other_array) end - # Deletes all the records from the collection. For +has_many+ associations, - # the deletion is done according to the strategy specified by the <tt>:dependent</tt> - # option. + # Deletes all the records 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. # - # 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 @@ -393,9 +399,9 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # If it is set to <tt>:destroy</tt> all the objects from the collection - # are removed by calling their +destroy+ method. See +destroy+ for more - # information. + # Both +has_many+ and +has_many :through+ dependencies default to the + # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+. + # Records are not instantiated and callbacks will not be fired. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :destroy @@ -410,11 +416,6 @@ module ActiveRecord # # ] # # person.pets.delete_all - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound @@ -443,8 +444,9 @@ module ActiveRecord end # Deletes the records of the collection directly from the database - # ignoring the +:dependent+ option. It invokes +before_remove+, - # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. + # ignoring the +:dependent+ option. Records are instantiated and it + # invokes +before_remove+, +after_remove+ , +before_destroy+ and + # +after_destroy+ callbacks. # # class Person < ActiveRecord::Base # has_many :pets @@ -690,10 +692,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - @association.count(column_name, options) + def count(column_name = nil) + @association.count(column_name) end # Returns the size of the collection. If the collection hasn't been loaded, diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb new file mode 100644 index 0000000000..fe48ecec29 --- /dev/null +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -0,0 +1,11 @@ +module ActiveRecord::Associations + module ForeignAssociation + def foreign_key_present? + if reflection.klass.primary_key + owner.attribute_present?(reflection.active_record_primary_key) + else + false + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 1413efaf7f..ca27c9fdde 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -16,7 +17,7 @@ module ActiveRecord unless empty? record = klass.human_attribute_name(reflection.name).downcase owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) - false + throw(:abort) end else @@ -66,7 +67,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - owner.read_attribute cached_counter_attribute_name + owner._read_attribute cached_counter_attribute_name else scope.count end @@ -84,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()) @@ -100,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 @@ -117,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 @@ -153,14 +168,6 @@ module ActiveRecord end end - def foreign_key_present? - if reflection.klass.primary_key - owner.attribute_present?(reflection.association_primary_key) - else - false - end - end - def concat_records(records, *) update_counter_if_success(super, records.length) end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 0968b0068e..4897ec44e9 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -11,21 +11,6 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query - # if the collection hasn't been loaded, and by calling collection.size if - # it has. If the collection will likely have a size greater than zero, - # and if fetching the collection will be needed afterwards, one less - # SELECT query will be generated by using #length instead. - def size - if has_cached_counter? - owner.read_attribute cached_counter_attribute_name(reflection) - elsif loaded? - target.size - else - super - end - end - def concat(*records) unless owner.new_record? records.flatten.each do |record| @@ -62,15 +47,7 @@ module ActiveRecord end save_through_record(record) - if has_cached_counter? && !through_reflection_updates_counter_cache? - ActiveSupport::Deprecation.warn \ - "Automatic updating of counter caches on through associations has been " \ - "deprecated, and will be removed in Rails 5.0. Instead, please set the " \ - "appropriate counter_cache options on the has_many and belongs_to for " \ - "your associations to #{through_reflection.name}." - update_counter_in_database(1) - end record end @@ -158,17 +135,15 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.to_a.each do |record| - record.run_callbacks :destroy - end + scope.each(&:_run_destroy_callbacks) arel = scope.arel - stmt = Arel::DeleteManager.new arel.engine + stmt = Arel::DeleteManager.new 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) @@ -185,9 +160,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) @@ -225,11 +200,6 @@ module ActiveRecord def invertible_for?(record) false end - - def through_reflection_updates_counter_cache? - counter_name = cached_counter_attribute_name - inverse_updates_counter_named?(counter_name, through_reflection) - end end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e6095d84dc..41a75b820e 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,6 +2,7 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -12,7 +13,7 @@ module ActiveRecord if load_target record = klass.human_attribute_name(reflection.name).downcase owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) - false + throw(:abort) end else diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index ec5c189cd3..81eb5136a1 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -20,7 +20,7 @@ module ActiveRecord end def columns - @tables.flat_map { |t| t.column_aliases } + @tables.flat_map(&:column_aliases) end # An array of [column_name, alias] pairs for the table @@ -93,8 +93,7 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins) - @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -142,11 +141,20 @@ module ActiveRecord parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root - result_set.each { |row_hash| - parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + message_bus = ActiveSupport::Notifications.instrumenter + + payload = { + record_count: result_set.length, + class_name: join_root.base_klass.name } + message_bus.instrument('instantiation.active_record', payload) do + result_set.each { |row_hash| + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + } + end + parents.values end @@ -224,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] @@ -248,6 +259,7 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model.readonly! seen[parent.base_klass][primary_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end 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 e7d3c9ba40..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 @@ -43,23 +43,30 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right end if rel && !rel.arel.constraints.empty? - bind_values.concat rel.bind_values + binds += rel.bound_attributes constraint = constraint.and rel.arel.constraints end @@ -67,8 +74,8 @@ module ActiveRecord value = foreign_klass.base_class.name column = klass.columns_hash[reflection.type.to_s] - substitute = klass.connection.substitute_at(column, bind_values.length) - bind_values.push [column, value] + substitute = klass.connection.substitute_at(column) + binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) constraint = constraint.and table[reflection.type].eq substitute end @@ -78,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/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 91e1c6a9d7..9c6573f913 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -19,7 +19,6 @@ module ActiveRecord def initialize(base_klass, children) @base_klass = base_klass - @column_names_with_alias = nil @children = children end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 46bccbf15a..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 @@ -178,6 +178,7 @@ module ActiveRecord class NullPreloader def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end + def self.preloaded_records; []; end end def preloader_for(reflection, owners, rhs_klass) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index c0639742be..1dc8bff193 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table @@ -131,25 +131,20 @@ 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] - - if preload_values.key? :order - scope.order! preload_values[:order] + if preload_scope.joins_values.any? + scope.joins!(preload_scope.joins_values) else - if values.key? :order - scope.order! values[:order] - end + scope.joins!(reflection_scope.joins_values) end + scope.order! preload_values[:order] || values[:order] if preload_values[:readonly] || values[:readonly] scope.readonly! @@ -159,6 +154,7 @@ module ActiveRecord scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end + scope.unscope_values = Array(values[:unscope]) klass.default_scoped.merge(scope) end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 7b37b5942d..2029871f39 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -8,7 +8,7 @@ module ActiveRecord records_by_owner = super if reflection_scope.distinct_value - records_by_owner.each_value { |records| records.uniq! } + records_by_owner.each_value(&:uniq!) end records_by_owner diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index d57da366bd..56aa23b173 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -78,9 +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.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 b9326b9683..c44242a0f0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,13 @@ module ActiveRecord end def get_records - return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) || scope.eager_loading? + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.limit(1).to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e47e81aa0f..3ce9cffdbc 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!( @@ -91,6 +91,17 @@ module ActiveRecord raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) end end + + def build_record(attributes) + inverse = source_reflection.inverse_of + target = through_association.target + + if inverse && target && !target.is_a?(Array) + attributes[inverse.foreign_key] = target.id + end + + super(attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 8cc1904575..73dd3fa041 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -9,6 +9,10 @@ module ActiveRecord FromUser.new(name, value, type) end + def with_cast_value(name, value, type) + WithCastValue.new(name, value, type) + end + def null(name) Null.new(name) end @@ -39,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) @@ -47,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) @@ -58,6 +62,14 @@ module ActiveRecord self.class.from_database(name, value, type) end + def with_cast_value(value) + 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 @@ -66,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 @@ -83,13 +108,27 @@ 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 + + class WithCastValue < Attribute # :nodoc: + def type_cast(value) + value + end + + def changed_in_place_from?(old_value) + false end end @@ -102,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 @@ -126,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_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 2887db3bf7..483d4200bd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,63 +3,32 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::ForbiddenAttributesProtection + include ActiveModel::AttributeAssignment - # 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? + # 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 - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } @@ -81,13 +50,18 @@ 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 end unless errors.empty? - error_descriptions = errors.map { |ex| ex.message }.join(",") + error_descriptions = errors.map(&:message).join(",") raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end @@ -113,100 +87,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 f4a4e3f605..9d58a19304 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/string/filters' require 'mutex_m' require 'thread_safe' @@ -82,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 @@ -90,7 +91,7 @@ module ActiveRecord def undefine_attribute_methods # :nodoc: generated_attribute_methods.synchronize do - super if @attribute_methods_generated + super if defined?(@attribute_methods_generated) && @attribute_methods_generated @attribute_methods_generated = false end end @@ -149,7 +150,7 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end - def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.respond_to?(name, true) if superklass.respond_to?(name, true) klass.method(name).owner != superklass.method(name).owner @@ -184,14 +185,15 @@ 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 end # Returns the column object for the named attribute. - # Returns nil if the named attribute does not exist. + # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the + # named attribute does not exist. # # class Person < ActiveRecord::Base # end @@ -201,21 +203,18 @@ module ActiveRecord # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) - # # => nil + # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> def column_for_attribute(name) - column = columns_hash[name.to_s] - if column.nil? - ActiveSupport::Deprecation.warn \ - "`column_for_attribute` will return a null object for non-existent columns " \ - "in Rails 5.0. Use `has_attribute?` if you need to check for an attribute's existence." + name = name.to_s + columns_hash.fetch(name) do + ConnectionAdapters::NullColumn.new(name) end - column end end # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. It also define the attribute methods if they have + # which will all return +true+. It also defines the attribute methods if they have # not been generated. # # class Person < ActiveRecord::Base @@ -329,7 +328,7 @@ module ActiveRecord # task.attribute_present?(:title) # => true # task.attribute_present?(:is_done) # => true def attribute_present?(attribute) - value = read_attribute(attribute) + value = _read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end @@ -370,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: @@ -430,7 +462,7 @@ module ActiveRecord end def typecasted_attribute_value(name) - read_attribute(name) + _read_attribute(name) end end end 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 2f02738f6d..7ba907f786 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -69,8 +69,17 @@ module ActiveRecord end end + def attribute_changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) + end + private + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) + end + def calculate_changes_from_defaults @changed_attributes = nil self.class.column_defaults.each do |attr, orig_value| @@ -99,7 +108,7 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) - if attribute_changed?(attr) + 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) @@ -110,7 +119,7 @@ module ActiveRecord if attribute_changed?(attr) changed_attributes[attr] else - clone_attribute_value(:read_attribute, attr) + clone_attribute_value(:_read_attribute, attr) end end @@ -122,10 +131,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) @@ -141,15 +148,10 @@ module ActiveRecord def changed_in_place self.class.attribute_names.select do |attr_name| - changed_in_place?(attr_name) + attribute_changed_in_place?(attr_name) end end - def changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) - end - def original_raw_attribute(attr_name) original_raw_attributes.fetch(attr_name) do read_attribute_before_type_cast(attr_name) @@ -161,7 +163,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 diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 9bd333bbac..c28374e4ab 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -17,7 +17,7 @@ module ActiveRecord def id if pk = self.class.primary_key sync_with_transaction_state - read_attribute(pk) + _read_attribute(pk) end end @@ -120,6 +120,7 @@ module ActiveRecord def primary_key=(value) @primary_key = value && value.to_s @quoted_primary_key = nil + @attributes_builder = nil 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 bf2a084a00..0d989c2eca 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Read @@ -27,7 +25,7 @@ module ActiveRecord <<-EOMETHOD def #{method_name} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - read_attribute(name) { |n| missing_attribute(n, caller) } + _read_attribute(name) { |n| missing_attribute(n, caller) } end EOMETHOD end @@ -36,60 +34,56 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| - define_method method_name do |*| - cached_attributes_deprecation_warning(method_name) - true - end - end - protected - def cached_attributes_deprecation_warning(method_name) - ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached." - end - - if Module.methods_transplantable? - def define_method_attribute(name) - method = ReaderMethodCache[name] - generated_attribute_methods.module_eval { define_method name, method } - end - else - def define_method_attribute(name) - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" - - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + def define_method_attribute(name) + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } end + STR + + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method end end end + ID = 'id'.freeze + # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) name = attr_name.to_s - name = self.class.primary_key if name == 'id' - @attributes.fetch_value(name, &block) + name = self.class.primary_key if name == ID + _read_attribute(name, &block) end - private - - def attribute(attribute_name) - read_attribute(attribute_name) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the read_attribute API + 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 + + alias :attribute :_read_attribute + private :attribute + end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 100d6d4229..d0d8a968c5 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -8,11 +8,8 @@ module ActiveRecord # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The # serialization is done through YAML. If +class_name+ is specified, the - # serialized object must be of that class on retrieval or - # <tt>SerializationTypeMismatch</tt> will be raised. - # - # A notable side effect of serialized attributes is that the model will - # be updated on every save, even if it is not dirty. + # serialized object must be of that class on assignment and retrieval. + # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # # ==== Parameters # @@ -52,17 +49,6 @@ module ActiveRecord Type::Serialized.new(type, coder) end end - - def serialized_attributes - ActiveSupport::Deprecation.warn "`serialized_attributes` is deprecated " \ - "without replacement, and will be removed in Rails 5.0." - - @serialized_attributes ||= Hash[ - columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| - [c.name, c.cast_type.coder] - } - ] - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index f439bd1ffe..f9beb43e4b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,19 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class TimeZoneConverter < SimpleDelegator # :nodoc: - def type_cast_from_database(value) + class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: + 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) - value.in_time_zone + begin + 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) } @@ -23,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 @@ -33,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 @@ -53,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_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index b3c8209a74..ab017c7b54 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Write @@ -25,27 +23,18 @@ module ActiveRecord module ClassMethods protected - if Module.methods_transplantable? - def define_method_attribute=(name) - method = WriterMethodCache[name] - generated_attribute_methods.module_eval { - define_method "#{name}=", method - } - end - else - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR end end @@ -73,7 +62,7 @@ module ActiveRecord if should_type_cast @attributes.write_from_user(attr_name, value) else - @attributes.write_from_database(attr_name, value) + @attributes.write_cast_value(attr_name, value) end value diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 98ac63c7e1..013a7d0e01 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -2,8 +2,6 @@ require 'active_record/attribute_set/builder' module ActiveRecord class AttributeSet # :nodoc: - delegate :keys, to: :initialized_attributes - def initialize(attributes) @attributes = attributes end @@ -12,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 @@ -25,8 +27,20 @@ module ActiveRecord attributes.key?(name) && self[name].initialized? end - def fetch_value(name, &block) - self[name].value(&block) + def keys + attributes.each_key.select { |name| self[name].initialized? } + end + + 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) @@ -37,13 +51,17 @@ module ActiveRecord attributes[name] = self[name].with_value_from_user(value) end + def write_cast_value(name, value) + attributes[name] = self[name].with_cast_value(value) + end + def freeze @attributes.freeze super end def initialize_dup(_) - @attributes = attributes.transform_values(&:dup) + @attributes = attributes.deep_dup super end @@ -58,10 +76,8 @@ module ActiveRecord end end - def ensure_initialized(key) - unless self[key].initialized? - write_from_database(key, nil) - end + def accessed + attributes.select { |_, attr| attr.has_been_read? }.keys end protected diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 1e146a07da..e85777c335 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,32 +1,92 @@ module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: - attr_reader :types + attr_reader :types, :always_initialized - def initialize(types) + def initialize(types, always_initialized = nil) @types = types + @always_initialized = always_initialized end def build_from_database(values = {}, additional_types = {}) - attributes = build_attributes_from_values(values, additional_types) - add_uninitialized_attributes(attributes) + if always_initialized && !values.key?(always_initialized) + values[always_initialized] = nil + end + + attributes = LazyAttributeHash.new(types, values, additional_types) AttributeSet.new(attributes) end + end + end + + class LazyAttributeHash # :nodoc: + delegate :transform_values, :each_key, to: :materialize + + def initialize(types, values, additional_types) + @types = types + @values = values + @additional_types = additional_types + @materialized = false + @delegate_hash = {} + end + + def key?(key) + delegate_hash.key?(key) || values.key?(key) || types.key?(key) + end - private + def [](key) + delegate_hash[key] || assign_default_value(key) + end - def build_attributes_from_values(values, additional_types) - values.each_with_object({}) do |(name, value), hash| - type = additional_types.fetch(name, types[name]) - hash[name] = Attribute.from_database(name, value, type) + def []=(key, value) + if frozen? + raise RuntimeError, "Can't modify frozen hash" + end + delegate_hash[key] = value + 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 + + private + + def assign_default_value(name) + type = additional_types.fetch(name, types[name]) + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + delegate_hash[name] = Attribute.from_database(name, value, type) + elsif types.key?(name) + delegate_hash[name] = Attribute.uninitialized(name, type) + end + end - def add_uninitialized_attributes(attributes) - types.except(*attributes.keys).each do |name, type| - attributes[name] = Attribute.uninitialized(name, type) + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true end end + delegate_hash end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 890a1314d9..c8979a60d7 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,29 +1,42 @@ 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: - self.user_provided_columns = {} + 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. + # + # +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. # - # +name+ The name of the methods to define attribute methods for, and the column which - # this will persist to. + # ==== Options + # The following options are accepted: # - # +cast_type+ A type object that contains information about how to type cast the value. - # See the examples section for more information. + # +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+. # - # ==== Options - # The options hash accepts the following options: + # +array+ (PG only) specifies that the type should be an array (see the examples below). # - # +default+ is the default value that the column should use on a new record. + # +range+ (PG only) specifies that the type should be a range (see the examples below). # # ==== Examples # @@ -44,78 +57,188 @@ 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" + # + # 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 in 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 => connection.new_column(name, options[:default], cast_type)) - 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+. + # + # +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 + 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 - def add_user_provided_columns(schema_columns) - existing_columns = schema_columns.map do |column| - user_provided_columns[column.name] || column + define_attribute(name, type, **options.slice(:default)) end + end - existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).values + private - existing_columns + new_columns - end + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED - def clear_caches_calculated_from_columns - @attributes_builder = nil - @column_names = nil - @column_types = nil - @columns = nil - @columns_hash = nil - @content_columns = nil - @default_attributes = nil + 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.from_user(name, value, type) + 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 a8e4d25df2..0792d19c3e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -177,14 +177,14 @@ 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) } - after_save save_method + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method elsif reflection.has_one? define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) # Configures two callbacks instead of a single after_save so that @@ -198,13 +198,29 @@ module ActiveRecord after_create save_method after_update save_method else - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false } 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) - define_non_cyclic_method(validation_method) { send(method, reflection) } + 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 + # callbacks is ignored, that is, returning `false` does not + # display a deprecation warning or halts the callback chain. + true + end validate validation_method end end @@ -261,20 +277,27 @@ module ActiveRecord if new_record association && association.target elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } + association.target.find_all(&:changed_for_autosave?) else - association.target.find_all { |record| record.new_record? } + association.target.find_all(&:new_record?) end end # 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? { |a| a.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 @@ -403,7 +426,9 @@ module ActiveRecord # If the record is new or it has changed, returns true. def record_changed?(reflection, record, key) - record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + record.new_record? || + (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || + record.attribute_changed?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f978fbd0a4..cc03e37a12 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' require 'active_record/attributes' +require 'active_record/type_caster' module ActiveRecord #:nodoc: # = Active Record @@ -141,7 +142,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. - # For numeric values, present is defined as non-zero. + # Additionally, when dealing with numeric values, a query method will return false if the value is zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -311,6 +312,8 @@ module ActiveRecord #:nodoc: 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 5955673b42..f44e5af5de 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -192,14 +192,14 @@ module ActiveRecord # # == <tt>before_validation*</tt> returning statements # - # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be + # 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. # # == Canceling callbacks # - # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are - # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled. + # If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and + # the associated action are cancelled. # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as # methods on the model, which are called last. # @@ -289,25 +289,25 @@ module ActiveRecord end def destroy #:nodoc: - run_callbacks(:destroy) { super } + _run_destroy_callbacks { super } end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private - def create_or_update #:nodoc: - run_callbacks(:save) { super } + def create_or_update(*) #:nodoc: + _run_save_callbacks { super } end def _create_record #:nodoc: - run_callbacks(:create) { super } + _run_create_callbacks { super } end def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index bc1a670b42..d99dc9a5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -121,13 +121,13 @@ module ActiveRecord # greater than the number of threads currently waiting (that # is, don't jump ahead in line). Otherwise, return nil. # - # If +timeout+ is given, block if it there is no element + # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to # become available. # # Raises: # - ConnectionTimeoutError if +timeout+ is given and no element - # becomes available after +timeout+ seconds, + # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize do if timeout @@ -150,7 +150,7 @@ module ActiveRecord end # A thread can remove an element from the queue without - # waiting if an only if the number of currently available + # waiting if and only if the number of currently available # connections is strictly greater than the number of waiting # threads. def can_remove_no_wait? @@ -235,7 +235,7 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 - @reaper = Reaper.new self, spec.config[:reaping_frequency] + @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) @reaper.run # default max pool size to 5 @@ -319,9 +319,7 @@ module ActiveRecord checkin conn conn.disconnect! if conn.requires_reloading? end - @connections.delete_if do |conn| - conn.requires_reloading? - end + @connections.delete_if(&:requires_reloading?) @available.clear @connections.each do |conn| @available.add conn @@ -360,11 +358,11 @@ module ActiveRecord synchronize do owner = conn.owner - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end - release owner + release conn, owner @available.add conn end @@ -377,7 +375,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn.owner + release conn, conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -425,10 +423,12 @@ module ActiveRecord end end - def release(owner) + def release(conn, owner) thread_id = owner.object_id - @reserved_connections.delete thread_id + if @reserved_connections[thread_id] == conn + @reserved_connections.delete thread_id + end end def new_connection @@ -449,10 +449,14 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c + rescue + remove c + c.disconnect! + raise end end @@ -516,14 +520,7 @@ module ActiveRecord def connection_pool_list owner_to_pool.values.compact end - - def connection_pools - ActiveSupport::Deprecation.warn( - "In the next release, this will return the same as #connection_pool_list. " \ - "(An array of pools, rather than a hash mapping specs to pools.)" - ) - Hash[connection_pool_list.map { |pool| [pool.spec, pool] }] - end + alias :connection_pools :connection_pool_list def establish_connection(owner, spec) @class_to_pool.clear @@ -640,7 +637,7 @@ module ActiveRecord end def call(env) - testing = env.key?('rack.test') + testing = env['rack.test'] response = @app.call(env) response[2] = ::Rack::BodyProxy.new(response[2]) do 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 98e96099cb..42ad285340 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -83,6 +83,11 @@ module ActiveRecord exec_query(sql, name, binds) end + # Executes the truncate statement. + def truncate(table_name, name = nil) + raise NotImplementedError + end + # Executes update +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. @@ -196,16 +201,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 @@ -229,6 +232,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 @@ -253,7 +260,18 @@ module ActiveRecord # Rolls back the transaction (and turns on auto-committing). Must be # done if the transaction block raises an exception or returns false. - def rollback_db_transaction() end + def rollback_db_transaction + exec_rollback_db_transaction + end + + def exec_rollback_db_transaction() end #:nodoc: + + def rollback_to_savepoint(name = nil) + exec_rollback_to_savepoint(name) + end + + def exec_rollback_to_savepoint(name = nil) #:nodoc: + end def default_sequence_name(table, column) nil @@ -269,10 +287,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' @@ -282,10 +307,6 @@ module ActiveRecord "DEFAULT VALUES" end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" - end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It @@ -332,8 +353,9 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) + exec_query(sql, name, binds) end - undef_method :select + # Returns the last auto-generated ID from the affected table. def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -362,7 +384,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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 4a4506c7f5..5e27cfe507 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -3,7 +3,7 @@ module ActiveRecord module QueryCache class << self def included(base) #:nodoc: - dirties_query_cache base, :insert, :update, :delete + dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction end def dirties_query_cache(base, *method_names) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index eb88845913..d2840b9498 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) @@ -19,13 +25,13 @@ module ActiveRecord # Cast a +value+ to a type that the database understands. For example, # SQLite does not understand dates, so this method will convert a Date # to a String. - def type_cast(value, column) + def type_cast(value, column = nil) if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end if column - value = column.cast_type.type_cast_for_database(value) + value = type_cast_from_column(column, value) end _type_cast(value) @@ -34,6 +40,40 @@ 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) @@ -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 @@ -87,7 +132,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 @@ -108,9 +162,8 @@ module ActiveRecord when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" + when Class then "'#{value}'" + else raise TypeError, "can't quote #{value.class.name}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index 25c17ce971..c0662f8473 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -9,7 +9,7 @@ module ActiveRecord execute("SAVEPOINT #{name}") end - def rollback_to_savepoint(name = current_savepoint_name) + def exec_rollback_to_savepoint(name = current_savepoint_name) execute("ROLLBACK TO SAVEPOINT #{name}") 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 6bab260f5a..f754df93b6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,11 +15,12 @@ module ActiveRecord end def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(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) @@ -30,9 +31,9 @@ module ActiveRecord end def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + 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 @@ -67,23 +68,13 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options 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_value(options[:default], options[:column])}" if options_include_default?(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 if options[:null] == false sql << " NOT NULL" @@ -91,16 +82,12 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end - def quote_value(value, column) - column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) - column.cast_type ||= type_for_column(column) - - @conn.quote(value, column) - end - def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end @@ -117,10 +104,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 fe00f9d750..0acc815d51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,14 +15,14 @@ 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, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end - class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: @@ -50,21 +50,130 @@ 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" end end - module TimestampDefaultDeprecation # :nodoc: - def emit_warning_if_null_unspecified(options) - return if options.key?(:null) + class ReferenceDefinition # :nodoc: + def initialize( + name, + polymorphic: false, + index: false, + foreign_key: false, + type: :integer, + **options + ) + @name = name + @polymorphic = polymorphic + @index = index + @foreign_key = foreign_key + @type = type + @options = options + + if polymorphic && foreign_key + raise ArgumentError, "Cannot add a foreign key to a polymorphic relation" + end + end + + def add_to(table) + columns.each do |column_options| + table.column(*column_options) + end + + if index + table.index(column_names, index_options) + end + + if foreign_key + table.foreign_key(foreign_table_name, foreign_key_options) + end + end + + protected + + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + + private + + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end + end - ActiveSupport::Deprecation.warn \ - "`timestamp` was called without specifying an option for `null`. In Rails " \ - "5.0, this behavior will change to `null: false`. You should manually " \ - "specify `null: true` to prevent the behavior of your existing migrations " \ - "from changing." + def polymorphic_options + as_options(polymorphic, options) + end + + def index_options + as_options(index) + end + + def foreign_key_options + as_options(foreign_key) + end + + def columns + result = [["#{name}_id", type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result + end + + def column_names + columns.map(&:first) + end + + def foreign_table_name + name.to_s.pluralize + 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 @@ -89,16 +198,17 @@ module ActiveRecord # The table definitions # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition - include TimestampDefaultDeprecation + include ColumnMethods # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as + attr_reader :name, :temporary, :options, :as, :foreign_keys def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} + @foreign_keys = {} @native = types @temporary = temporary @options = options @@ -108,12 +218,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] @@ -124,8 +228,8 @@ module ActiveRecord # 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>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, - # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. # # You may use a type not in this list as long as it is supported by your # database (for example, "polygon" in MySQL), but this will not be database @@ -226,7 +330,7 @@ module ActiveRecord # t.integer :shop_id, :creator_id # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" - # t.timestamps + # t.timestamps null: false # end # # There's a short-hand method for each of the type values declared at the top. And then there's @@ -256,7 +360,7 @@ module ActiveRecord name = name.to_s type = type.to_sym - if primary_key_column_name == name + 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." end @@ -270,14 +374,6 @@ module ActiveRecord @columns_hash.delete name.to_s end - [: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! - 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 # @@ -286,39 +382,43 @@ module ActiveRecord indexes[column_name] = options end + def foreign_key(table_name, options = {}) # :nodoc: + foreign_keys[table_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. + # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # + # t.timestamps null: false def timestamps(*args) options = args.extract_options! - emit_warning_if_null_unspecified(options) + + options[:null] = false if options[:null].nil? + column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # 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. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # # See SchemaStatements#add_reference - def references(*args) - options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer + def references(*args, **options) args.each do |col| - column("#{col}_id", type, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + ReferenceDefinition.new(col, **options).add_to(self) end end alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: - type = aliased_types[type] || type + type = aliased_types(type.to_s, type) column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) @@ -331,6 +431,7 @@ module ActiveRecord column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -340,19 +441,12 @@ module ActiveRecord ColumnDefinition.new name, type end - def primary_key_column_name - primary_key_column = columns.detect { |c| c.primary_key? } - primary_key_column && primary_key_column.name - end - def native @native end - def aliased_types - HashWithIndifferentAccess.new( - timestamp: :datetime, - ) + def aliased_types(name, fallback) + 'timestamp' == name ? :datetime : fallback end end @@ -391,6 +485,7 @@ module ActiveRecord # Available transformations are: # # change_table :table do |t| + # t.primary_key # t.column # t.index # t.rename_index @@ -403,6 +498,7 @@ module ActiveRecord # t.string # t.text # t.integer + # t.bigint # t.float # t.decimal # t.datetime @@ -419,132 +515,145 @@ module ActiveRecord # end # class Table - include TimestampDefaultDeprecation + include ColumnMethods + + attr_reader :name def initialize(table_name, base) - @table_name = table_name + @name = table_name @base = base end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) - @base.add_column(@table_name, column_name, type, options) + @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) - @base.column_exists?(@table_name, column_name, type, options) + @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) - @base.add_index(@table_name, column_name, options) + @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) - @base.index_exists?(@table_name, column_name, options) + @base.index_exists?(name, column_name, options) end # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) - @base.rename_index(@table_name, index_name, new_index_name) + @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. + # + # t.timestamps(null: false) # - # t.timestamps + # See SchemaStatements#add_timestamps def timestamps(options = {}) - emit_warning_if_null_unspecified(options) - @base.add_timestamps(@table_name, options) + @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) - @base.change_column(@table_name, column_name, type, options) + @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) - @base.change_column_default(@table_name, column_name, default) + @base.change_column_default(name, column_name, default) end # Removes the column(s) from the table definition. # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) - @base.remove_columns(@table_name, *column_names) + @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) - @base.remove_index(@table_name, options) + @base.remove_index(name, options) end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps - def remove_timestamps - @base.remove_timestamps(@table_name) + # + # See SchemaStatements#remove_timestamps + def remove_timestamps(options = {}) + @base.remove_timestamps(name, options) end # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) - @base.rename_column(@table_name, column_name, new_column_name) + @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. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # Adds a reference. Optionally adds a +type+ column, if + # <tt>:polymorphic</tt> option is provided. # # 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 def references(*args) options = args.extract_options! args.each do |ref_name| - @base.add_reference(@table_name, ref_name, options) + @base.add_reference(name, ref_name, options) end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) @@ -553,22 +662,17 @@ module ActiveRecord def remove_references(*args) options = args.extract_options! args.each do |ref_name| - @base.remove_reference(@table_name, ref_name, options) + @base.remove_reference(name, ref_name, options) end end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type - # - # t.string(:goat) - # t.string(:goat, :sheep) - [: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 |name| - @base.add_column(@table_name, name, column_type, options) - end - end + def foreign_key(*args) # :nodoc: + @base.add_foreign_key(name, *args) + end + + def foreign_key_exists?(*args) # :nodoc: + @base.foreign_key_exists?(name, *args) end private 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 b05a4f8440..af7ef7cbaa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -6,22 +6,28 @@ module ActiveRecord # We can then redefine how certain data types may be handled in the schema dumper on the # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper - def column_spec(column, types) - spec = prepare_column_options(column, types) - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + def column_spec(column) + spec = prepare_column_options(column) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")} spec end + def column_spec_for_primary_key(column) + return if column.type == :integer + spec = { id: column.type.inspect } + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) + end + # This can be overridden on a Adapter level basis to support other # extended datatypes (Example: Adding an array option in the # PostgreSQLAdapter) - def prepare_column_options(column, types) + def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect spec[:type] = column.type.to_s spec[:null] = 'false' unless column.null - limit = column.limit || types[column.type][:limit] + 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 @@ -40,9 +46,10 @@ module ActiveRecord private 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 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 7105df1ee4..d42f9a894b 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: @@ -132,6 +134,7 @@ module ActiveRecord # Make a temporary table. # [<tt>:force</tt>] # Set to true to drop the table before creating it. + # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is @@ -203,7 +206,17 @@ module ActiveRecord end result = execute schema_creation.accept td - td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + + unless supports_indexes_in_create? + td.indexes.each_pair do |column_name, index_options| + add_index(table_name, column_name, index_options) + end + end + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + result end @@ -361,11 +374,15 @@ module ActiveRecord # Drops a table from the database. # - # Although this command ignores +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. + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # 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. @@ -571,6 +588,8 @@ module ActiveRecord # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' # def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def @@ -619,17 +638,16 @@ module ActiveRecord # # add_belongs_to(:products, :supplier, polymorphic: true) # - # ====== Create a supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # - def add_reference(table_name, ref_name, options = {}) - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer - add_column(table_name, "#{ref_name}_id", type, options) - add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + # ====== Create a supplier_id column and appropriate foreign key + # + # add_reference(:products, :supplier, foreign_key: true) + # + def add_reference(table_name, *args) + ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -644,7 +662,13 @@ 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 = {}) + remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] end @@ -733,21 +757,7 @@ module ActiveRecord 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] } - - 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 @@ -755,6 +765,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 @@ -778,7 +813,7 @@ module ActiveRecord version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i } + migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } versions = Dir[*paths].map do |filename| filename.split('/').last.split('_').first.to_i @@ -816,6 +851,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 @@ -835,11 +876,14 @@ module ActiveRecord columns end - # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+. + # Additional options (like <tt>null: false</tt>) are forwarded to #add_column. # - # add_timestamps(:suppliers) + # add_timestamps(:suppliers, null: false) # def add_timestamps(table_name, options = {}) + options[:null] = false if options[:null].nil? + add_column table_name, :created_at, :datetime, options add_column table_name, :updated_at, :datetime, options end @@ -848,7 +892,7 @@ module ActiveRecord # # remove_timestamps(:suppliers) # - def remove_timestamps(table_name) + def remove_timestamps(table_name, options = {}) remove_column table_name, :updated_at remove_column table_name, :created_at end @@ -962,17 +1006,25 @@ module ActiveRecord end private - def create_table_definition(name, temporary, options, as = nil) + def create_table_definition(name, temporary = false, options = nil, as = nil) TableDefinition.new native_database_types, name, temporary, options, as end def create_alter_table(name) - AlterTable.new create_table_definition(name, false, {}) + AlterTable.new create_table_definition(name) 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) + 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 end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 90be835d8a..3a1e4a4a88 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -55,11 +55,7 @@ module ActiveRecord 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,16 +65,11 @@ module ActiveRecord def rollback_records ite = records.uniq while record = ite.shift - begin - record.rolledback! full_rollback? - rescue => e - raise if ActiveRecord::Base.raise_in_transactional_callbacks - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end + 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 @@ -86,19 +77,18 @@ module ActiveRecord @state.set_state(:committed) end + def before_commit_records + records.uniq.each(&:before_committed!) + end + def commit_records ite = records.uniq while record = ite.shift - begin - record.committed! - rescue => e - raise if ActiveRecord::Base.raise_in_transactional_callbacks - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end + record.committed! end ensure ite.each do |i| - i.committed!(false) + i.committed!(should_run_callbacks: false) end end @@ -121,14 +111,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 @@ -148,13 +135,11 @@ module ActiveRecord def rollback connection.rollback_db_transaction super - rollback_records end def commit connection.commit_db_transaction super - commit_records end end @@ -171,16 +156,30 @@ module ActiveRecord else SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) end + @stack.push(transaction) transaction end def commit_transaction - @stack.pop.commit + inner_transaction = @stack.pop + + if current_transaction.joinable? + inner_transaction.commit + inner_transaction.records.each do |r| + r.add_to_transaction + end + else + inner_transaction.before_commit_records + inner_transaction.commit + inner_transaction.commit_records + end 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 = {}) @@ -190,11 +189,17 @@ module ActiveRecord rollback_transaction if transaction raise ensure - begin - commit_transaction unless error - rescue Exception - transaction.rollback unless transaction.state.completed? - raise + unless error + if Thread.current.status == 'aborting' + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index a0d9086875..ae42e8ef8d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ 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' @@ -14,16 +15,14 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload_at 'active_record/connection_adapters/column' do - autoload :Column - autoload :NullColumn - end + autoload :Column autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -115,7 +114,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 @@ -245,6 +245,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 @@ -263,12 +268,10 @@ module ActiveRecord {} end - # QUOTING ================================================== - - # Returns a bind substitution value given a bind +index+ and +column+ + # 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, index) - Arel::Nodes::BindParam.new '?' + def substitute_at(column, _unused = 0) + Arel::Nodes::BindParam.new end # REFERENTIAL INTEGRITY ==================================== @@ -343,9 +346,6 @@ module ActiveRecord def create_savepoint(name = nil) end - def rollback_to_savepoint(name = nil) - end - def release_savepoint(name = nil) end @@ -360,8 +360,17 @@ 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 @@ -378,26 +387,30 @@ 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) + Column.new(name, default, sql_type_metadata, null) end def lookup_cast_type(sql_type) # :nodoc: type_map.lookup(sql_type) end + def column_name_for_operation(operation, node) # :nodoc: + visitor.accept(node, collector).value + end + 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' @@ -431,6 +444,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 @@ -443,12 +463,21 @@ 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) - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.error message if @logger + begin + message = "#{e.class.name}: #{e.message}: #{sql}" + rescue Encoding::CompatibilityError + message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" + end + 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 037fb69dfb..c084431588 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,31 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include Savepoints + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint + super + end + 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 + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -31,12 +56,8 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - column = o.column - options = o.options - sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) - change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" - add_column_options!(change_column_sql, options.merge(column: column)) - add_column_position!(change_column_sql, options) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) end def add_column_position!(sql, options) @@ -54,18 +75,38 @@ module ActiveRecord 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.auto_increment? + return unless column.limit == 8 + spec[:id] = ':bigint' + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + def prepare_column_options(column) + spec = super + spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 + spec.delete(:limit) if :boolean === column.type + spec + end + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict, :extra + delegate :strict, :collation, :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 @@ -73,7 +114,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 @@ -91,6 +132,10 @@ module ActiveRecord collation && !collation.match(/_ci$/) end + def auto_increment? + extra == 'auto_increment' + end + private # MySQL misreports NOT NULL column default when none is given. @@ -111,6 +156,34 @@ module ActiveRecord end end + class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :collation, :extra, :strict + + def initialize(type_metadata, collation: "", extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @collation = collation + @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 + [self.class, @type_metadata, collation, extra, strict] + end + end + ## # :singleton-method: # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> @@ -162,6 +235,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 @@ -201,6 +284,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 @@ -217,8 +304,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) # :nodoc: + Column.new(field, default, sql_type_metadata, null) end # Must return the MySQL error number from the exception, if the exception has an @@ -274,7 +361,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def clear_cache! super @@ -311,7 +400,7 @@ module ActiveRecord execute "COMMIT" end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: execute "ROLLBACK" end @@ -385,10 +474,14 @@ module ActiveRecord sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| - result.collect { |field| field.first } + result.collect(&:first) end end + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + end + def table_exists?(name) return false unless name.present? return true if tables(nil, nil, name).any? @@ -435,8 +528,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[:Collation], field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") end end end @@ -470,18 +563,20 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + 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) if supports_rename_index? + validate_index_length!(table_name, new_name) + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super end end - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) #:nodoc: column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -570,14 +665,6 @@ module ActiveRecord end end - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') @@ -624,10 +711,6 @@ module ActiveRecord end end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql - end - def strict_mode? self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end @@ -641,11 +724,7 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super - m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] - .split(',').map{|enum| enum.strip.length - 2}.max - Type::String.new(limit: limit) - end + register_class_with_limit m, %r(char)i, MysqlString m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) @@ -655,18 +734,47 @@ module ActiveRecord m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) - m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) - m.register_type %r(^int)i, Type::Integer.new(limit: 4) - m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) - m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) - m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + register_integer_type m, %r(^bigint)i, limit: 8 + register_integer_type m, %r(^int)i, limit: 4 + register_integer_type m, %r(^mediumint)i, limit: 3 + register_integer_type m, %r(^smallint)i, limit: 2 + register_integer_type m, %r(^tinyint)i, limit: 1 + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' + + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + MysqlString.new(limit: limit) + end + end + + def register_integer_type(mapping, key, options) # :nodoc: + mapping.register_type(key) do |sql_type| + if /unsigned/i =~ sql_type + Type::UnsignedInteger.new(options) + else + Type::Integer.new(options) + end + end + end + + def extract_precision(sql_type) + if /time/ === sql_type + super || 0 + else + super + end + end + + def fetch_type_metadata(sql_type, collation = "", extra = "") + MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -717,7 +825,7 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options] + td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) schema_creation.visit_AddColumn cd end @@ -733,21 +841,23 @@ module ActiveRecord options[:null] = column.null end - options[:name] = column.name - schema_creation.accept ChangeColumnDefinition.new column, type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(column.name, type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def rename_column_sql(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { - name: new_column_name, 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"] - schema_creation.accept ChangeColumnDefinition.new column, current_type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(new_column_name, current_type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def remove_column_sql(table_name, column_name, type = nil, options = {}) @@ -772,14 +882,14 @@ module ActiveRecord [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name) + def remove_timestamps_sql(table_name, options = {}) [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end private def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) end def mariadb? @@ -821,9 +931,9 @@ module ActiveRecord # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| if v == ':default' || v == :default - "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default + "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? - "@@SESSION.#{k.to_s} = #{quote(v)}" + "@@SESSION.#{k} = #{quote(v)}" end # or else nil; compact to clear nils out end.compact.join(', ') @@ -840,6 +950,33 @@ module ActiveRecord end end end + + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + + class MysqlString < Type::String # :nodoc: + def serialize(value) + case value + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else super + 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 5f9cc6edd0..fa5ed07b8a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,7 +5,6 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set module Format @@ -13,30 +12,22 @@ 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 - delegate :type, :precision, :scale, :limit, :klass, :accessor, - :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) - @name = name - @cast_type = cast_type - @sql_type = sql_type - @null = null - @default = default - @default_function = nil + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + @name = name + @sql_type_metadata = sql_type_metadata + @null = null + @default = default + @default_function = default_function end def has_default? @@ -51,10 +42,26 @@ module ActiveRecord Base.human_attribute_name(@name) end - def with_type(type) - dup.tap do |clone| - clone.instance_variable_set('@cast_type', type) - end + def ==(other) + other.is_a?(Column) && + attributes_for_hash == other.attributes_for_hash + end + alias :eql? :== + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, name, default, sql_type_metadata, null, default_function] + end + end + + class NullColumn < Column + def initialize(name) + super(name, nil) end end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index d28a54b8f9..08d46fca96 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -33,7 +33,7 @@ module ActiveRecord def initialize(url) raise "Database URL cannot be empty" if url.blank? @uri = uri_parser.parse(url) - @adapter = @uri.scheme.gsub('-', '_') + @adapter = @uri.scheme.tr('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -209,27 +209,13 @@ module ActiveRecord when Symbol resolve_symbol_connection spec when String - resolve_string_connection spec + resolve_url_connection spec when Hash resolve_hash_connection spec end end - def resolve_string_connection(spec) - # Rails has historically accepted a string to mean either - # an environment key or a URL spec, so we have deprecated - # this ambiguous behaviour and in the future this function - # can be removed in favor of resolve_url_connection. - if configurations.key?(spec) || spec !~ /:/ - ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ - "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" - resolve_symbol_connection(spec) - else - resolve_url_connection(spec) - end - end - - # Takes the environment such as `:production` or `:development`. + # Takes the environment such as +:production+ or +:development+. # This requires that the @configurations was initialized with a key that # matches. # diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 38bdddefba..21631be25c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -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 @@ -66,21 +57,17 @@ module ActiveRecord exception.error_number if exception.respond_to?(:error_number) end + #-- # QUOTING ================================================== + #++ def quote_string(string) @connection.escape(string) end - def quoted_date(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? return false unless @connection @@ -104,7 +91,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds.dup)}" @@ -232,11 +221,6 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an ActiveRecord::Result instance. - def select(sql, name = nil, binds = []) - exec_query(sql, name) - end - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) super id_value || @connection.last_id diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index da3aecf69a..64985ee933 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -58,7 +58,7 @@ module ActiveRecord # * <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 `SET @@SESSION.key = value` 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>: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>: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. @@ -88,7 +88,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| hash[:stmt].close end cache.clear @@ -137,7 +137,9 @@ module ActiveRecord @connection.quote(string) end + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? if @connection.respond_to?(:stat) @@ -178,7 +180,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true @@ -324,8 +328,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: @@ -391,11 +395,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 @@ -406,7 +408,7 @@ 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 @@ -419,9 +421,7 @@ module ActiveRecord cols = nil if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } + cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) metadata.free end @@ -465,7 +465,7 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds) + rows = super @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 37e5c3859c..0eb4fb468c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -2,18 +2,11 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - attr_accessor :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 - super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) - else - @array = false - super(name, default, cast_type, sql_type, null) - end - - @default_function = default_function + def serial? + default_function && default_function =~ /\Anextval\(.*\)\z/ end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 89a7257d77..11d3f5301a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -156,10 +156,6 @@ module ActiveRecord end end - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - def exec_query(sql, name = 'SQL', binds = []) execute_and_clear(sql, name, binds) do |result| types = {} @@ -227,7 +223,7 @@ module ActiveRecord end # Aborts a transaction. - def rollback_db_transaction + def exec_rollback_db_transaction execute "ROLLBACK" 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..92349e2f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,19 @@ -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/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 cd5efe2bb8..fb4e0de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -18,26 +18,29 @@ module ActiveRecord end attr_reader :subtype, :delimiter - delegate :type, to: :subtype + delegate :type, :user_input_in_time_zone, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @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(parse_pg_array(value), :deserialize) else super end end - def type_cast_from_user(value) - type_cast_array(value, :type_cast_from_user) + def cast(value) + if value.is_a?(::String) + value = parse_pg_array(value) + end + 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) else @@ -45,6 +48,12 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Array) && + subtype == other.subtype && + delimiter == other.delimiter + end + private def type_cast_array(value, method) @@ -60,7 +69,7 @@ module ActiveRecord casted_values = value.map { |item| cast_value_for_database(item) } "{#{casted_values.join(delimiter)}}" else - quote_and_escape(subtype.type_cast_for_database(value)) + quote_and_escape(subtype.serialize(value)) 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 997613d7be..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,8 +3,9 @@ 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) end end 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 a53b4ee8e2..eeccb09bdf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -12,15 +12,15 @@ module ActiveRecord # If the subnet mask is equal to /32, don't output it if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" + "\"#{value}\"" else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + "\"#{value}/#{subnet_mask.to_s(2).count('1')}\"" end end - def type_cast_for_database(value) + def serialize(value) if IPAddr === value - "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else value end 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..b3b610a5f6 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,7 @@ module ActiveRecord :enum end - def type_cast(value) + def cast(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..afc9383f91 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -13,7 +13,7 @@ module ActiveRecord # 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/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 84b9490ba3..fc201f8fb9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -5,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 @@ -23,20 +25,12 @@ module ActiveRecord to = type_cast_single extracted[:to] if !infinity?(from) && extracted[:exclude_start] - if from.respond_to?(:succ) - from = from.succ - ActiveSupport::Deprecation.warn \ - "Excluding the beginning of a Range is only partialy supported " \ - "through `#succ`. This is not reliable and will be removed in " \ - "the future." - else - raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" - end + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" end ::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) @@ -46,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/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 e396ff4a1e..9b3de41fab 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 @@ -4,7 +4,7 @@ module ActiveRecord module OID # :nodoc: # This class uses the data from PostgreSQL pg_type table to build # the OID -> Type mapping. - # - OID is and integer representing the type. + # - OID is an integer representing the type. # - Type is an OID::Type object. # This class has side effects on the +store+ passed during initialization. class TypeMapInitializer # :nodoc: @@ -39,14 +39,14 @@ module ActiveRecord end def register_array_type(row) - if subtype = @store.lookup(row['typelem'].to_i) - register row['oid'], OID::Array.new(subtype, row['typdelim']) + register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype| + OID::Array.new(subtype, row['typdelim']) end end def register_range_type(row) - if subtype = @store.lookup(row['rngsubtype'].to_i) - register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype| + OID::Range.new(subtype, row['typname'].to_sym) end end @@ -64,9 +64,13 @@ module ActiveRecord end end - def register(oid, oid_type) - oid = assert_valid_registration(oid, oid_type) - @store.register_type(oid, oid_type) + def register(oid, oid_type = nil, &block) + oid = assert_valid_registration(oid, oid_type || block) + if block_given? + @store.register_type(oid, &block) + else + @store.register_type(oid, oid_type) + end end def alias_type(oid, target) @@ -74,6 +78,14 @@ module ActiveRecord @store.alias_type(oid, target) end + def register_with_subtype(oid, target_oid) + if @store.key?(target_oid) + register(oid) do |_, *args| + yield @store.lookup(target_oid, *args) + end + end + end + def assert_valid_registration(oid, oid_type) raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? oid.to_i 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 033e0324bb..5e839228e9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -3,23 +3,16 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: - RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [1-5][a-fA-F0-9]{3}-? - [8-Bab][a-fA-F0-9]{3}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-?\}?\z}x + 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) - value.to_s[RFC_4122, 0] + def cast(value) + value.to_s[ACCEPTABLE_UUID, 0] end 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 cf5c8d288e..b7755c4593 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -14,22 +14,6 @@ module ActiveRecord @connection.unescape_bytea(value) if value end - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Float - if value.infinite? || value.nan? - "'#{value.to_s}'" - else - super - end - else - super - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -59,27 +43,30 @@ module ActiveRecord # 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: - result = super - if value.acts_like?(:time) && value.respond_to?(:usec) - result = "#{result}.#{sprintf("%06d", value.usec)}" - end - if value.year <= 0 bce_year = format("%04d", -value.year + 1) - result = result.sub(/^-?\d+/, bce_year) + " BC" + super.sub(/^-?\d+/, bce_year) + " BC" + else + super end - result 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 + elsif column.respond_to?(:array?) + value = type_cast_from_column(column, value) + quote(value) else - quote(value, column) + 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) @@ -94,6 +81,12 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end + when Float + if value.infinite? || value.nan? + "'#{value}'" + else + super + end else super end 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..c1835380f9 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 => 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(";")) + transaction(require_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end rescue - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";")) 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 b37630a04c..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,41 +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 = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options - 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 767b6b614a..75b9d079bd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,40 +4,9 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) - end - sql - end - - def add_column_options!(sql, options) - if options[:array] || options[:column].try(:array) - sql << '[]' - end - - column = options.fetch(:column) { return super } - if column.type == :uuid && options[:default] =~ /\(\)/ - sql << " DEFAULT #{options[:default]}" - else - super - end - end - - def type_for_column(column) - if column.array - @conn.lookup_cast_type("#{column.sql_type}[]") - else - super - end + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) + super end end @@ -60,8 +29,8 @@ module ActiveRecord def create_database(name, options = {}) options = { encoding: 'utf8' }.merge!(options.symbolize_keys) - option_string = options.sum do |key, value| - case key + option_string = options.inject("") do |memo, (key, value)| + memo += case key when :owner " OWNER = \"#{value}\"" when :template @@ -118,6 +87,10 @@ module ActiveRecord SQL end + def drop_table(table_name, options = {}) + 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 @@ -186,15 +159,17 @@ module ActiveRecord 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) + 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 == 'f', default_function) 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) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) end # Returns the current database name. @@ -293,6 +268,23 @@ module ActiveRecord result.rows.first.first end + # Sets the sequence of a table's primary key to the specified value. + def set_pk_sequence!(table, value) #:nodoc: + pk, sequence = pk_and_sequence_for(table) + + if pk + if sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'SCHEMA' + SELECT setval('#{quoted_sequence}', #{value}) + end_sql + else + @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + end + end + end + # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: unless pk and sequence @@ -371,15 +363,15 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first + pks = exec_query(<<-end_sql, 'SCHEMA').rows SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql - - row && row.first + return nil unless pks.count == 1 + pks[0][0] end # Renames a table. @@ -394,7 +386,10 @@ module ActiveRecord pk, seq = pk_and_sequence_for(new_name) if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" + idx = "#{table_name}_pkey" + new_idx = "#{new_name}_pkey" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end rename_table_indexes(table_name, new_name) @@ -411,9 +406,16 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) 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] - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + 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[: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 change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -431,7 +433,7 @@ 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 @@ -439,7 +441,7 @@ module ActiveRecord 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 @@ -461,12 +463,14 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end def foreign_keys(table_name) fk_info = select_all <<-SQL.strip_heredoc - SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid JOIN pg_class t2 ON c.confrelid = t2.oid @@ -488,6 +492,7 @@ module ActiveRecord options[:on_delete] = extract_foreign_key_action(row['on_delete']) options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) end end @@ -505,8 +510,8 @@ 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. @@ -522,24 +527,18 @@ module ActiveRecord 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 @@ -555,6 +554,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/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 0290bcb48c..9a0b80d7d3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -18,7 +18,11 @@ module ActiveRecord end def quoted - parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + if schema + PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + else + PGconn.quote_ident(identifier) + end end def ==(o) @@ -32,8 +36,11 @@ module ActiveRecord protected def unquote(part) - return unless part - part.gsub(/(^"|"$)/,'') + if part && part.start_with?('"') + part[1..-2] + else + part + end end def parts @@ -57,7 +64,11 @@ module ActiveRecord # * <tt>"schema_name".table_name</tt> # * <tt>"schema.name"."table name"</tt> def extract_schema_qualified_name(string) - table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + schema, table = string.scan(/[^".\s]+|"[^"]*"/) + if table.nil? + table = schema + schema = nil + end PostgreSQL::Name.new(schema, table) 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 80461f3910..6d25b53b21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,19 +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 "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 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.11' +gem 'pg', '~> 0.15' require 'pg' require 'ipaddr' @@ -64,7 +64,7 @@ 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 @@ -78,6 +78,7 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", + bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -94,6 +95,7 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, + bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -102,6 +104,7 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, + jsonb: { name: "jsonb" }, ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, @@ -122,16 +125,31 @@ module ActiveRecord PostgreSQL::SchemaCreation.new self end - # Adds `:array` option to the default set provided by the + def column_spec_for_primary_key(column) + spec = {} + if column.serial? + return unless column.sql_type == 'bigint' + spec[:id] = ':bigserial' + elsif column.type == :uuid + spec[:id] = ':uuid' + spec[:default] = column.default_function.inspect + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + # Adds +:array+ option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) # :nodoc: + def prepare_column_options(column) # :nodoc: spec = super - spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec[:array] = 'true' if column.array? spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end - # Adds `:array` as a valid migration key + # Adds +:array+ as a valid migration key def migration_keys super + [:array] end @@ -162,6 +180,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -237,6 +259,8 @@ module ActiveRecord @table_alias_length = nil connect + add_pg_decoders + @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -255,6 +279,10 @@ module ActiveRecord @statements.clear end + def truncate(table_name, name = nil) + exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, [] + end + # Is this connection alive and ready for queries? def active? @connection.query 'SELECT 1' @@ -387,6 +415,16 @@ module ActiveRecord super(oid) end + def column_name_for_operation(operation, node) # :nodoc: + OPERATION_ALIASES.fetch(operation) { operation.downcase } + end + + OPERATION_ALIASES = { # :nodoc: + "maximum" => "max", + "minimum" => "min", + "average" => "avg", + } + protected # Returns the version of the connected PostgreSQL server. @@ -427,11 +465,11 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', OID::Integer - m.alias_type 'int4', 'int2' - m.alias_type 'int8', 'int2' + 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 @@ -442,8 +480,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 @@ -469,10 +506,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) @@ -499,14 +534,17 @@ module ActiveRecord def extract_limit(sql_type) # :nodoc: case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super + when /^bigint/i, /^int8/i + 8 + when /^smallint/i + 2 + else + super end 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 @@ -515,7 +553,7 @@ module ActiveRecord when 'true', 'false' default # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ $1 # Object identifier types when /\A-?\d+\z/ @@ -574,14 +612,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.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) - @connection.block - @connection.get_last_result + log(sql, name, binds, stmt_key) do + @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e pgerror = e.original_exception @@ -673,9 +707,9 @@ module ActiveRecord variables.map do |k, v| if v == ':default' || v == :default # Sets the value to the global or compile default - execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA') + execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA') elsif !v.nil? - execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA') + execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA') end end end @@ -693,12 +727,6 @@ module ActiveRecord exec_query("SELECT currval('#{sequence_name}')", 'SQL') end - # Executes a SELECT query and returns the results, performing any data type - # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: @@ -734,9 +762,76 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) # :nodoc: + 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) == 't' + end + end + 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, + } + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + FROM pg_type as t + SQL + coders = execute_and_clear(query, "SCHEMA", []) do |result| + result + .map { |row| construct_coder(row, coders_by_name['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'], 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(: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 a10ce330c7..37ff4e4613 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -61,9 +61,7 @@ module ActiveRecord end def size - [@columns, @columns_hash, @primary_keys, @tables].map { |x| - x.size - }.inject :+ + [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ end # Clear out internal caches for table with +table_name+. 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_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ebb311df57..7e184dd510 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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). # @@ -88,11 +69,11 @@ module ActiveRecord include Comparable def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } + @version = version_string.split('.').map(&:to_i) end def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } + @version <=> version_string.split('.').map(&:to_i) end end @@ -115,7 +96,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| dealloc hash[:stmt] end cache.clear @@ -140,6 +121,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 @@ -179,10 +161,6 @@ module ActiveRecord true end - def supports_add_column? - true - end - def supports_views? true end @@ -243,6 +221,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 @@ -257,20 +241,12 @@ module ActiveRecord end def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - 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) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end + @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" @@ -292,11 +268,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) @@ -314,7 +288,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) @@ -363,7 +337,7 @@ module ActiveRecord log('commit transaction',nil) { @connection.commit } end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: log('rollback transaction',nil) { @connection.rollback } end @@ -399,8 +373,8 @@ module ActiveRecord end 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) end end @@ -430,10 +404,9 @@ module ActiveRecord end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] + pks = table_structure(table_name).select { |f| f['pk'] > 0 } + return nil unless pks.count == 1 + pks[0]['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -451,12 +424,12 @@ module ActiveRecord # See: http://www.sqlite.org/lang_altertable.html # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) + def valid_alter_table_type?(type) type.to_sym != :primary_key end def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) + if valid_alter_table_type?(type) super(table_name, column_name, type, options) else alter_table(table_name) do |definition| @@ -508,16 +481,6 @@ 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 select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? @@ -562,7 +525,7 @@ module ActiveRecord end copy_table_indexes(from, to, options[:rename] || {}) copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, + @definition.columns.map(&:name), options[:rename] || {}) end @@ -575,7 +538,7 @@ module ActiveRecord name = name[1..-1] end - to_column_names = columns(to).map { |c| c.name } + to_column_names = columns(to).map(&:name) columns = index.columns.map {|c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -592,25 +555,14 @@ module ActiveRecord def copy_table_contents(from, to, columns, rename = {}) #:nodoc: column_mappings = Hash[columns.map {|name| [name, name]}] rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} + from_columns = columns(from).collect(&:name) columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' - quoted_to = quote_table_name(to) - - raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] - - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - - column_values = columns.map do |col| - quote(row[column_mappings[col]], raw_column_mappings[col]) - end - - sql << column_values * ', ' - sql << ')' - exec_query sql - end + exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) + SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 31e7390bf7..984af79642 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { Rails.env if defined?(Rails) } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 83859e474a..d41872d767 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,6 +1,7 @@ +require 'thread' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/duplicable' -require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord module Core @@ -84,16 +85,11 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true - # :nodoc: mattr_accessor :maintain_test_schema, instance_accessor: false - def self.disable_implicit_join_references=(value) - ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ - "Make sure to remove this configuration because it does nothing.") - end + 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 @@ -112,45 +108,50 @@ 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 return super if block_given? || primary_key.nil? || default_scopes.any? || + current_scope || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) id = ids.first if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end + 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 record + rescue RangeError + raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end - def find_by(*args) + def find_by(*args) # :nodoc: return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? return super if default_scopes.any? @@ -160,29 +161,31 @@ module ActiveRecord v.nil? || Array === v || Hash === v } - key = hash.keys + # We can't cache Post.find_by(author: david) ...yet + return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } + + 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 + nil end end - def find_by!(*args) - find_by(*args) or raise RecordNotFound + 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 @@ -203,7 +206,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)" @@ -221,7 +224,7 @@ module ActiveRecord # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: - @arel_table ||= Arel::Table.new(table_name, arel_engine) + @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end # Returns the Arel engine. @@ -234,10 +237,24 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + + def type_caster # :nodoc: + TypeCaster::Map.new(self) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + 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) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -245,6 +262,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with @@ -255,19 +276,17 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil, options = {}) - @attributes = self.class.default_attributes.dup + def initialize(attributes = nil) + @attributes = self.class._default_attributes.dup + self.class.define_attribute_methods init_internals initialize_internals_callback - self.class.define_attribute_methods - # +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_callbacks :initialize unless _initialize_callbacks.empty? + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ must contain @@ -289,8 +308,8 @@ module ActiveRecord self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + _run_find_callbacks + _run_initialize_callbacks self end @@ -326,10 +345,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) unless _initialize_callbacks.empty? - - @aggregation_cache = {} - @association_cache = {} + _run_initialize_callbacks @new_record = true @destroyed = false @@ -439,6 +455,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? } @@ -464,16 +481,16 @@ module ActiveRecord Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end + private + def set_transaction_state(state) # :nodoc: @transaction_state = state end def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_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 @@ -496,6 +513,8 @@ module ActiveRecord end def update_attributes_from_transaction_state(transaction_state, depth) + @reflects_state = [false] if depth == 0 + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? unless @reflects_state[depth] restore_transaction_record_state if transaction_state.rolledback? @@ -522,10 +541,6 @@ module ActiveRecord end def init_internals - @attributes.ensure_initialized(self.class.primary_key) - - @aggregation_cache = {} - @association_cache = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -534,22 +549,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 f0b6afc4b4..82596b63df 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -20,7 +20,7 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |counter_association| - has_many_association = _reflect_on_association(counter_association.to_sym) + has_many_association = _reflect_on_association(counter_association) unless has_many_association has_many = reflect_on_all_associations(:has_many) has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } @@ -37,10 +37,9 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count(:all) - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end @@ -123,16 +122,6 @@ module ActiveRecord end end - protected - - def actually_destroyed? - @_actually_destroyed - end - - def clear_destroy_state - @_actually_destroyed = nil - end - private def _create_record(*) @@ -167,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/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e94b74063e..b6dd6814db 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,10 +1,5 @@ module ActiveRecord module DynamicMatchers #:nodoc: - # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the activerecord-deprecated_finders - # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), - # then we can remove the indirection. - def respond_to?(name, include_private = false) if self == Base super @@ -72,26 +67,14 @@ module ActiveRecord CODE end - def body - raise NotImplementedError - end - end + private - module Finder - # Extended in activerecord-deprecated_finders def body - result - end - - # Extended in activerecord-deprecated_finders - def result "#{finder}(#{attributes_hash})" end # The parameters in the signature may have reserved Ruby words, in order # to prevent errors, we start each param name with `_`. - # - # Extended in activerecord-deprecated_finders def signature attribute_names.map { |name| "_#{name}" }.join(', ') end @@ -109,7 +92,6 @@ module ActiveRecord class FindBy < Method Method.matchers << self - include Finder def self.prefix "find_by" @@ -122,7 +104,6 @@ module ActiveRecord class FindByBang < Method Method.matchers << self - include Finder def self.prefix "find_by" diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 5958373e88..ea88983917 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 doesn'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,17 @@ 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. + module Enum def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) @@ -79,6 +87,38 @@ 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 definitions.each do |name, values| @@ -90,37 +130,19 @@ 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| enum_values[value] = i # def active?() status == 0 end klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == i } + define_method("#{value}?") { self[name] == value.to_s } # def active!() update! status: :active end klass.send(:detect_enum_conflict!, name, "#{value}!") @@ -128,7 +150,7 @@ module ActiveRecord # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => i } + klass.scope value, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values @@ -138,25 +160,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 52c70977ef..d710d96a9a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -52,10 +52,29 @@ module ActiveRecord # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be # saved because record is invalid. class RecordNotSaved < ActiveRecordError + attr_reader :record + + def initialize(message, record = nil) + @record = record + super(message) + end end # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # + # begin + # complex_operation_that_internally_calls_destroy! + # rescue ActiveRecord::RecordNotDestroyed => invalid + # puts invalid.record.errors + # end + # class RecordNotDestroyed < ActiveRecordError + attr_reader :record + + def initialize(record) + @record = record + super() + end end # Superclass for all database execution errors. @@ -159,24 +178,17 @@ module ActiveRecord class DangerousAttributeError < ActiveRecordError 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}") - end - - end + UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc: + 'ActiveRecord::UnknownAttributeError', + 'ActiveModel::AttributeAssignment::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 # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute + def initialize(message, exception, attribute) super(message) @exception = exception @@ -189,6 +201,7 @@ module ActiveRecord # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors + def initialize(errors) @errors = errors end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 4527452f1a..75b0e1e08d 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,7 +132,7 @@ 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: @@ -181,6 +182,9 @@ module ActiveRecord # * Stable, autogenerated IDs # * Label references for associations (belongs_to, has_one, has_many) # * HABTM associations as inline lists + # + # There are some more advanced features available even if the id is specified: + # # * Autofilled timestamp columns # * Fixture label interpolation # * Support for YAML defaults @@ -518,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.keys.each do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + table_rows.each_key do |table| + 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| @@ -630,7 +638,7 @@ module ActiveRecord # interpolate the fixture label row.each do |key, value| - row[key] = value.gsub("$LABEL", label) if value.is_a?(String) + row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String) end # generate a primary key if necessary @@ -646,7 +654,7 @@ module ActiveRecord model_class end - reflection_class._reflections.values.each do |association| + reflection_class._reflections.each_value do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same @@ -658,7 +666,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.columns_hash[association.foreign_key].type + fk_type = association.active_record.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -688,7 +696,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 @@ -700,6 +708,10 @@ module ActiveRecord def lhs_key @association.through_reflection.foreign_key end + + def join_table + @association.through_reflection.table_name + end end private @@ -708,7 +720,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) @@ -742,7 +754,7 @@ module ActiveRecord end def column_names - @column_names ||= @connection.columns(@table_name).collect { |c| c.name } + @column_names ||= @connection.columns(@table_name).collect(&:name) end def read_fixture_files(path, model_class) @@ -765,12 +777,6 @@ module ActiveRecord end - #-- - # Deprecate 'Fixtures' in favor of 'FixtureSet'. - #++ - # :nodoc: - Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet') - class Fixture #:nodoc: include Enumerable @@ -803,7 +809,9 @@ module ActiveRecord def find if model_class - model_class.find(fixture[model_class.primary_key]) + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) + end else raise FixtureClassNotFound, "No class attached to find." end @@ -863,38 +871,13 @@ module ActiveRecord fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else - fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map(&:to_s) end self.fixture_table_names |= fixture_set_names - require_fixture_classes(fixture_set_names, self.config) setup_fixture_accessors(fixture_set_names) end - def try_to_load_dependency(file_name) - require_dependency file_name - rescue LoadError => e - unless fixture_class_names.key?(file_name.pluralize) - if ActiveRecord::Base.logger - ActiveRecord::Base.logger.warn("Unable to load #{file_name}, make sure you added it to ActiveSupport::TestCase.set_fixture_class") - ActiveRecord::Base.logger.warn("underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}") - end - end - end - - def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base) - if fixture_set_names - fixture_set_names = fixture_set_names.map { |n| n.to_s } - else - fixture_set_names = fixture_table_names - end - - fixture_set_names.each do |file_name| - file_name = file_name.singularize if config.pluralize_table_names - try_to_load_dependency(file_name) - end - end - def setup_fixture_accessors(fixture_set_names = nil) fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do @@ -908,7 +891,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] @@ -928,7 +911,7 @@ module ActiveRecord def uses_transaction(*methods) @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.concat methods.map { |m| m.to_s } + @uses_transaction.concat methods.map(&:to_s) end def uses_transaction?(method) @@ -971,7 +954,7 @@ module ActiveRecord end # Instantiate fixtures for every test if requested. - instantiate_fixtures(config) if use_instantiated_fixtures + instantiate_fixtures if use_instantiated_fixtures end def teardown_fixtures @@ -998,16 +981,9 @@ module ActiveRecord Hash[fixtures.map { |f| [f.name, f] }] end - # for pre_loaded_fixtures, only require the classes once. huge speed improvement - @@required_fixture_classes = false - - def instantiate_fixtures(config) + def instantiate_fixtures if pre_loaded_fixtures raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? - unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config - @@required_fixture_classes = true - end ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 15c9dee712..a388b529c9 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -5,10 +5,10 @@ module ActiveRecord end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 251d682a02..24098f72dc 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -79,16 +79,6 @@ module ActiveRecord :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) end - def symbolized_base_class - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.") - @symbolized_base_class ||= base_class.to_s.to_sym - end - - def symbolized_sti_name - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.") - @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class - end - # Returns the class descending directly from ActiveRecord::Base, or # an abstract class, if any, in the inheritance hierarchy. # @@ -192,7 +182,7 @@ module ActiveRecord def type_condition(table = arel_table) sti_column = table[inheritance_column] - sti_names = ([self] + descendants).map { |model| model.sti_name } + sti_names = ([self] + descendants).map(&:sti_name) sti_column.in(sti_names) end @@ -202,7 +192,7 @@ 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) 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 52eeb8ae1f..a09437b4b0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -11,7 +11,7 @@ module ActiveRecord # # == Usage # - # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the + # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example: # @@ -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? @@ -80,17 +89,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attributes_for_update(attribute_names).map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end @@ -118,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.bind_values.length) - - 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 @@ -141,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 @@ -181,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/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index eb64d197f0..6b26d7be78 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -20,18 +20,14 @@ 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) @@ -47,9 +43,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 d0d9304a36..a83b90a95f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -39,7 +39,7 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize - if defined?(Rails) + if defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") @@ -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 # @@ -184,8 +184,8 @@ module ActiveRecord # you wish to downgrade. Alternatively, you can also use the STEP option if you # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback # the latest two migrations. - # - # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, + # + # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, # that step will fail and you'll have some manual work to do. # # == Database support @@ -394,8 +394,15 @@ module ActiveRecord end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? - ActiveRecord::Tasks::DatabaseTasks.load_schema_current + if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + # 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! + system("bin/rake db:test:prepare") + # Establish a new connection, the old database may be gone (db:test:prepare uses purge) + Base.establish_connection(current_config) + end check_pending! end end @@ -640,7 +647,7 @@ module ActiveRecord end def method_missing(method, *arguments, &block) - arg_list = arguments.map{ |a| a.inspect } * ', ' + arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert @@ -848,6 +855,10 @@ module ActiveRecord (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 end + def any_migrations? + migrations(migrations_paths).any? + end + def last_version last_migration.version end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 850220babd..75adcccce6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -63,7 +63,7 @@ module ActiveRecord # records, artists => artists_records # music_artists, music_records => music_artists_records def self.derive_join_table_name(first_table, second_table) # :nodoc: - [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") end module ClassMethods @@ -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 @@ -147,7 +133,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -231,33 +217,42 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(column_types) + @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 - default_attributes.to_hash + load_schema + _default_attributes.to_hash end - def default_attributes # :nodoc: - @default_attributes ||= attributes_builder.build_from_database( - columns_hash.transform_values(&:default)) + def _default_attributes # :nodoc: + @default_attributes ||= AttributeSet.new({}) end # Returns an array of column names as strings. def column_names - @column_names ||= columns.map { |column| column.name } + @column_names ||= columns.map(&:name) end # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", @@ -295,22 +290,53 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? - - @arel_engine = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @default_attributes = nil - @dynamic_methods_hash = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil - @time_zone_column_names = nil - @cached_time_zone = nil + connection.schema_cache.clear_table_cache!(table_name) + + 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| + 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 + @column_names = nil + @attribute_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = 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 diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8a2a06f2ca..084ef397a8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -81,6 +81,9 @@ module ActiveRecord # # Note that the model will _not_ be destroyed until the parent is saved. # + # Also note that the model will not be destroyed unless you also specify + # its id in the updated hash. + # # === One-to-many # # Consider a member that has a number of posts: @@ -111,7 +114,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # You may also set a :reject_if proc to silently ignore any new record + # You may also set a +:reject_if+ proc to silently ignore any new record # hashes if they fail to pass your criteria. For example, the previous # example could be rewritten as: # @@ -133,7 +136,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # Alternatively, :reject_if also accepts a symbol for using methods: + # Alternatively, +:reject_if+ also accepts a symbol for using methods: # # class Member < ActiveRecord::Base # has_many :posts @@ -212,13 +215,13 @@ module ActiveRecord # All changes to models, including the destruction of those marked for # destruction, are saved and destroyed automatically and atomically when # the parent model is saved. This happens inside the transaction initiated - # by the parents save method. See ActiveRecord::AutosaveAssociation. + # by the parent's save method. See ActiveRecord::AutosaveAssociation. # # === Validating the presence of a parent model # # If you want to validate that a child record is associated with a parent - # record, you can use <tt>validates_presence_of</tt> and - # <tt>inverse_of</tt> as this example illustrates: + # record, you can use the +validates_presence_of+ method and the +:inverse_of+ + # key as this example illustrates: # # class Member < ActiveRecord::Base # has_many :posts, inverse_of: :member @@ -230,7 +233,7 @@ module ActiveRecord # validates_presence_of :member # end # - # Note that if you do not specify the <tt>inverse_of</tt> option, then + # Note that if you do not specify the +:inverse_of+ option, then # Active Record will try to automatically guess the inverse association # based on heuristics. # @@ -264,29 +267,31 @@ module ActiveRecord # Allows you to specify a Proc or a Symbol pointing to a method # that checks whether a record should be built for a certain attribute # hash. The hash is passed to the supplied Proc or the method - # and it should return either +true+ or +false+. When no :reject_if + # and it should return either +true+ or +false+. When no +:reject_if+ # is specified, a record will be built for all attribute hashes that # do not have a <tt>_destroy</tt> value that evaluates to true. # Passing <tt>:all_blank</tt> instead of a Proc will create a proc # that will reject a record where all the attributes are blank excluding - # any value for _destroy. + # any value for +_destroy+. # [:limit] - # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. Limit also can be specified as a - # Proc or a Symbol pointing to a method that should return number. If the size of the - # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords - # exception is raised. If omitted, any number associations can be processed. - # Note that the :limit option is only applicable to one-to-many associations. + # Allows you to specify the maximum number of associated records that + # can be processed with the nested attributes. Limit also can be specified + # as a Proc or a Symbol pointing to a method that should return a number. + # If the size of the nested attributes array exceeds the specified limit, + # NestedAttributes::TooManyRecords exception is raised. If omitted, any + # number of associations can be processed. + # Note that the +:limit+ option is only applicable to one-to-many + # associations. # [:update_only] # For a one-to-one association, this option allows you to specify how - # nested attributes are to be used when an associated record already + # nested attributes are going to be used when an associated record already # exists. In general, an existing record may either be updated with the # new set of attribute values or be replaced by a wholly new record - # containing those values. By default the :update_only option is +false+ + # containing those values. By default the +:update_only+ option is +false+ # and the nested attributes are used to update the existing record only # if they include the record's <tt>:id</tt> value. Otherwise a new # record will be instantiated and used to replace the existing one. - # However if the :update_only option is +true+, the nested attributes + # However if the +:update_only+ option is +true+, the nested attributes # are used to update the record's attributes always, regardless of # whether the <tt>:id</tt> is present. The option is ignored for collection # associations. @@ -307,7 +312,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 @@ -516,7 +521,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/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index dbf4564ae5..edb5066fa0 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,7 +45,7 @@ module ActiveRecord NoTouching.applied_to?(self.class) end - def touch(*) + def touch(*) # :nodoc: super unless no_touching? end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 807c301596..fa6eaa7e64 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -30,10 +30,18 @@ module ActiveRecord true end + def none? + true + end + def any? false end + def one? + false + end + def many? false end @@ -62,9 +70,7 @@ module ActiveRecord calculate :maximum, nil end - def calculate(operation, _column_name, _options = {}) - # TODO: Remove _options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, _column_name) if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? @@ -77,5 +83,9 @@ module ActiveRecord def exists?(_id = false) 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 755ff2b2f1..7c076864a3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -109,37 +109,45 @@ module ActiveRecord # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # - # There's a series of callbacks associated with +save+. If any of the - # <tt>before_*</tt> callbacks return +false+ the action is cancelled and - # +save+ returns +false+. See ActiveRecord::Callbacks for further + # By default, #save also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # + # There's a series of callbacks associated with #save. If any of the + # <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled and + # #save returns +false+. See ActiveRecord::Callbacks for further # details. # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations # for more information. # - # There's a series of callbacks associated with <tt>save!</tt>. If any of - # the <tt>before_*</tt> callbacks return +false+ the action is cancelled - # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See + # By default, #save! also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # + # There's a series of callbacks associated with #save!. If any of + # the <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled + # and #save! raises ActiveRecord::RecordNotSaved. See # ActiveRecord::Callbacks for further details. # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -149,6 +157,8 @@ module ActiveRecord # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # + # Note that this will also delete records marked as <tt>readonly?</tt>. + # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. @@ -161,13 +171,14 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). # - # There's a series of callbacks associated with <tt>destroy</tt>. If - # the <tt>before_destroy</tt> callback return +false+ the action is cancelled - # and <tt>destroy</tt> returns +false+. See - # ActiveRecord::Callbacks for further details. + # There's a series of callbacks associated with #destroy. If the + # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled + # and #destroy returns +false+. + # See ActiveRecord::Callbacks for further details. def destroy - raise ReadOnlyRecord if readonly? + 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 @@ -176,12 +187,12 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). # - # There's a series of callbacks associated with <tt>destroy!</tt>. If - # the <tt>before_destroy</tt> callback return +false+ the action is cancelled - # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See - # ActiveRecord::Callbacks for further details. + # There's a series of callbacks associated with #destroy!. If the + # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled + # and #destroy! raises ActiveRecord::RecordNotDestroyed. + # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed) + destroy || raise(ActiveRecord::RecordNotDestroyed, self) end # Returns an instance of the specified +klass+ with the attributes of the @@ -197,7 +208,8 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) + changed_attributes = @changed_attributes if defined?(@changed_attributes) + became.instance_variable_set("@changed_attributes", changed_attributes || {}) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) @@ -235,8 +247,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 @@ -342,7 +354,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 @@ -403,9 +415,6 @@ module ActiveRecord # end # def reload(options = nil) - clear_aggregation_cache - clear_association_cache - fresh_object = if options && options[:lock] self.class.unscoped { self.class.lock(options[:lock]).find(id) } @@ -418,14 +427,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 # @@ -449,19 +461,18 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names) + def touch(*names, time: current_time_from_proper_timezone) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? 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? @@ -485,20 +496,12 @@ 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, 0) - - 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 - raise ReadOnlyRecord if readonly? - result = new_record? ? _create_record : _update_record + def create_or_update(*args) + raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index a9ddd9141f..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 @@ -37,19 +37,30 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = result_set.column_types.except(*columns_hash.keys) - result_set.map { |record| instantiate(record, column_types) } + column_types = result_set.column_types.dup + columns_hash.each_key { |k| column_types.delete k } + message_bus = ActiveSupport::Notifications.instrumenter + + payload = { + record_count: result_set.length, + class_name: name + } + + message_bus.instrument('instantiation.active_record', payload) do + result_set.map { |record| instantiate(record, column_types) } + end end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. # The use of this method should be restricted to complicated SQL queries that can't be executed # using the ActiveRecord::Calculations class methods. Look into those before using this. # - # ==== Parameters + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + # # => 12 # - # * +sql+ - An SQL statement which should return a count query from the database, see the example below. + # ==== Parameters # - # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + # * +sql+ - An SQL statement which should return a count query from the database, see the example above. def count_by_sql(sql) sql = sanitize_conditions(sql) connection.select_value(sql, "#{name} Count").to_i diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a4ceacbf44..b6de90e89d 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,8 +36,6 @@ module ActiveRecord config.eager_load_namespaces << ActiveRecord rake_tasks do - require "active_record/base" - namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration @@ -60,6 +58,9 @@ module ActiveRecord require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" console = ActiveSupport::Logger.new(STDERR) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + Rails.logger.extend ActiveSupport::Logger.broadcast console end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 458862a538..2591e7492d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -196,7 +196,8 @@ db_namespace = namespace :db do fixture_files = if ENV['FIXTURES'] ENV['FIXTURES'].split(',') else - Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s } + # The use of String#[] here is to support namespaced fixtures + Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] } end ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) @@ -215,7 +216,7 @@ db_namespace = namespace :db do Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) - data.keys.each do |key| + data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) if key == label || key_id == id.to_i @@ -239,7 +240,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => [:environment, :load_config] do + task :load => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -268,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) @@ -285,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 @@ -304,7 +305,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from the current schema" - task :load => %w(db:test:deprecated db:test:purge) do + task :load => %w(db:test:purge) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:load_schema"].invoke @@ -314,11 +315,11 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => %w(db:test:deprecated db:test:purge) do + task :load_schema => %w(db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -327,8 +328,8 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => %w(db:test:deprecated db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] + task :load_structure => %w(db:test:purge) do + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" @@ -348,12 +349,12 @@ db_namespace = namespace :db do task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => %w(db:test:deprecated environment load_config) do + task :purge => %w(environment load_config) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => %w(db:test:deprecated environment load_config) do + task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -365,7 +366,7 @@ namespace :railties do namespace :install do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do - to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } + to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip) railties = {} Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 85bbac43e4..ce78f1756d 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -11,7 +11,7 @@ module ActiveRecord # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6b5a592ee5..dab5a502a5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord # = Active Record Reflection @@ -38,9 +39,9 @@ module ActiveRecord ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end - # \Reflection enables interrogating of Active Record classes and objects - # about their associations and aggregations. This information can, - # for example, be used in a form builder that takes an Active Record object + # \Reflection enables the ability to examine the associations and aggregations of + # Active Record classes and objects. This information, for example, + # can be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. # @@ -62,7 +63,7 @@ module ActiveRecord # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. # - # Account.reflections # => {balance: AggregateReflection} + # Account.reflections # => {"balance" => AggregateReflection} # # @api public def reflections @@ -148,16 +149,19 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - def join_keys(assoc_klass) + def join_keys(association_klass) JoinKeys.new(foreign_key, active_record_primary_key) end - def source_macro - ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ - "will be removed without replacement.") - macro + def constraints + scope_chain.flatten + end + + def alias_candidate(name) + "#{plural_name}_#{name}" end end + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # @@ -273,7 +277,7 @@ module ActiveRecord def initialize(name, scope, options, active_record) super @automatic_inverse_of = nil - @type = options[:as] && "#{options[:as]}_type" + @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @@ -283,7 +287,7 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements if polymorphic? - key = [key, owner.read_attribute(@foreign_type)] + key = [key, owner._read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @association_scope_cache[key] ||= yield @@ -339,13 +343,11 @@ module ActiveRecord return unless scope if scope.arity > 0 - ActiveSupport::Deprecation.warn \ - "The association scope '#{name}' is instance dependent (the scope " \ - "block takes an argument). Preloading happens before the individual " \ - "instances are created. This means that there is no instance being " \ - "passed to the association scope. This will most likely result in " \ - "broken or incorrect behavior. Joining, Preloading and eager loading " \ - "of these associations is deprecated and will be removed in the future." + raise ArgumentError, <<-MSG.squish + The association scope '#{name}' is instance dependent (the scope + block takes an argument). Preloading instance dependent scopes is + not supported. + MSG end end alias :check_eager_loadable! :check_preloadable! @@ -494,7 +496,7 @@ module ActiveRecord # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym begin reflection = klass._reflect_on_association(inverse_name) @@ -596,8 +598,8 @@ module ActiveRecord def belongs_to?; true; end - def join_keys(assoc_klass) - key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key + def join_keys(association_klass) + key = polymorphic? ? association_primary_key(association_klass) : association_primary_key JoinKeys.new(key, foreign_key) end @@ -693,6 +695,11 @@ module ActiveRecord @chain ||= begin a = source_reflection.chain b = through_reflection.chain + + if options[:source_type] + b[0] = PolymorphicReflection.new(b[0], self) + end + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain @@ -740,15 +747,8 @@ module ActiveRecord end end - def join_keys(assoc_klass) - source_reflection.join_keys(assoc_klass) - end - - # The macro used by the source association - def source_macro - ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ - "will be removed without replacement.") - source_reflection.source_macro + def join_keys(association_klass) + source_reflection.join_keys(association_klass) end # A through association is nested if there would be more than one join table @@ -783,7 +783,7 @@ module ActiveRecord def source_reflection_name # :nodoc: return @source_reflection_name if @source_reflection_name - names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq + names = [name.to_s.singularize, name].collect(&:to_sym).uniq names = names.find_all { |n| through_reflection.klass._reflect_on_association(n) } @@ -821,7 +821,11 @@ module ActiveRecord end if through_reflection.polymorphic? - raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + if has_one? + raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self) + else + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end end if source_reflection.nil? @@ -843,6 +847,12 @@ module ActiveRecord check_validity_of_inverse! end + def constraints + scope_chain = source_reflection.constraints + scope_chain << scope if scope + scope_chain + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -865,5 +875,81 @@ module ActiveRecord delegate(*delegate_methods, to: :delegate_reflection) end + + class PolymorphicReflection < ThroughReflection # :nodoc: + def initialize(reflection, previous_reflection) + @reflection = reflection + @previous_reflection = previous_reflection + end + + def klass + @reflection.klass + end + + def scope + @reflection.scope + end + + def table_name + @reflection.table_name + end + + def plural_name + @reflection.plural_name + end + + def join_keys(association_klass) + @reflection.join_keys(association_klass) + end + + def type + @reflection.type + end + + def constraints + [source_type_info] + end + + def source_type_info + type = @previous_reflection.foreign_type + source_type = @previous_reflection.options[:source_type] + lambda { |object| where(type => source_type) } + end + end + + class RuntimeReflection < PolymorphicReflection # :nodoc: + attr_accessor :next + + def initialize(reflection, association) + @reflection = reflection + @association = association + end + + def klass + @association.klass + end + + def table_name + klass.table_name + end + + def constraints + @reflection.constraints + end + + def source_type_info + @reflection.source_type_info + end + + def alias_candidate(name) + "#{plural_name}_#{name}_join" + end + + def alias_name + Arel::Table.new(table_name) + end + + def all_includes; yield; end + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ad54d84665..6bbec7c0c0 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,40 +1,39 @@ # -*- coding: utf-8 -*- -require 'arel/collectors/bind' +require "arel/collectors/bind" module ActiveRecord # = Active Record Relation class Relation - JoinOperation = Struct.new(:relation, :join_class, :on) - 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, + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :uniq] + 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 - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false + @predicate_builder = predicate_builder end def initialize_copy(other) # 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,22 +80,26 @@ module ActiveRecord scope.unscope!(where: @klass.inheritance_column) end - um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + relation = scope.where(@klass.primary_key => (id_was || id)) + bvs = binds + relation.bound_attributes + um = relation + .arel + .compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, 'SQL', - binds) + bvs, + ) end def substitute_values(values) # :nodoc: - substitutes = values.sort_by { |arel_attr,_| arel_attr.name } - binds = substitutes.map do |arel_attr, value| - [@klass.columns_hash[arel_attr.name], value] + binds = values.map do |arel_attr, value| + QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) end - substitutes.each_with_index do |tuple, i| - tuple[1] = @klass.connection.substitute_at(binds[i][0], i) + substitutes = values.map do |(arel_attr, _)| + [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] end [substitutes, binds] @@ -268,6 +271,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? @@ -277,6 +289,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? @@ -302,10 +323,11 @@ module ActiveRecord klass.current_scope = previous end - # Updates all records with details given if they match a set of conditions supplied, limits and order can - # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the - # database. It does not instantiate the involved models and it does not trigger Active Record callbacks - # or validations. + # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE + # statement and sends it straight to the database. It does not instantiate the involved models and it does not + # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through + # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL + # database. # # ==== Parameters # @@ -324,7 +346,7 @@ module ActiveRecord def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? - stmt = Arel::UpdateManager.new(arel.engine) + stmt = Arel::UpdateManager.new stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) @@ -338,8 +360,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - bvs = bind_values + arel.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. @@ -358,9 +379,21 @@ module ActiveRecord # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) - def update(id, attributes) + # + # # Updates multiple records from the result of a relation + # people = Person.where(group: 'expert') + # people.update(group: 'masters') + # + # Note: Updating a large number of records will run a + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use <tt>update_all</tt> for updating all records using + # a single query. + def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } + elsif id == :all + to_a.each { |record| record.update(attributes) } else object = find(id) object.update(attributes) @@ -397,7 +430,7 @@ module ActiveRecord if conditions where(conditions).destroy_all else - to_a.each {|object| object.destroy }.tap { reset } + to_a.each(&:destroy).tap { reset } end end @@ -451,8 +484,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? @@ -462,7 +497,7 @@ module ActiveRecord if conditions where(conditions).delete_all else - stmt = Arel::DeleteManager.new(arel.engine) + stmt = Arel::DeleteManager.new stmt.from(table) if joins_values.any? @@ -471,7 +506,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 @@ -541,10 +576,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 @@ -554,22 +589,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) - else - where.right.val - end - }] - }] + where_clause.to_h(relation_table_name) end def scope_for_create @@ -632,21 +652,25 @@ 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? - preloader = ActiveRecord::Associations::Preloader.new + preloader = build_preloader preload.each do |associations| preloader.preload @records, associations end - @records.each { |record| record.readonly! } if readonly_value + @records.each(&:readonly!) if readonly_value @loaded = true @records end + def build_preloader + ActiveRecord::Associations::Preloader.new + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) @@ -659,7 +683,7 @@ module ActiveRecord joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq (references_values - joined_tables).any? end @@ -668,7 +692,7 @@ module ActiveRecord return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index b069cdce7c..e07580a563 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,37 +27,46 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <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 # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # 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,34 +86,38 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <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 # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # 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 90e99957f6..4a4de86d48 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,15 +19,15 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - # - # If +count+ is used with +group+ for multiple columns, it returns a Hash whose - # keys are an array containing the individual values of each column and the value + # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value # of each key would be the +count+. - # + # # Article.group(:status, :category).count - # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, # ["published", "business"]=>0, ["published", "technology"]=>2} - # + # # If +count+ is used with +select+, it will count the selected columns: # # Person.select(:age).count @@ -35,21 +35,16 @@ module ActiveRecord # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + def count(column_name = nil) + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:average, column_name, options) + def average(column_name) + calculate(:average, column_name) end # Calculates the minimum value on a given column. The value is returned @@ -57,10 +52,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:minimum, column_name, options) + def minimum(column_name) + calculate(:minimum, column_name) end # Calculates the maximum value on a given column. The value is returned @@ -68,10 +61,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:maximum, column_name, options) + def maximum(column_name) + calculate(:maximum, column_name) end # Calculates the sum of values on a given column. The value is returned @@ -114,17 +105,15 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + construct_relation_for_association_calculations.calculate(operation, column_name) else - perform_calculation(operation, column_name, options) + perform_calculation(operation, column_name) end end @@ -177,8 +166,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, 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 @@ -196,9 +185,7 @@ module ActiveRecord eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def perform_calculation(operation, column_name) operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) @@ -240,27 +227,24 @@ module ActiveRecord 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) select_value = operation_over_aggregate_column(column, operation, distinct) column_alias = select_value.alias + column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) 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 @@ -274,7 +258,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass._reflect_on_association(group_attrs.first.to_sym) + association = @klass._reflect_on_association(group_attrs.first) associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else @@ -302,7 +286,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) @@ -316,11 +300,11 @@ module ActiveRecord relation.group_values = group relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, 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 @@ -369,9 +353,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 @@ -390,11 +374,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 50f4d5c7ab..d4a8823cfe 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,6 +1,5 @@ require 'set' require 'active_support/concern' -require 'active_support/deprecation' module ActiveRecord module Delegation # :nodoc: diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b1753b27e1..fb47d915ff 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,5 @@ require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' module ActiveRecord module FinderMethods @@ -81,12 +82,16 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(*args) where(*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! + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" end # Gives a record (or N records if a parameter is supplied) without any implied @@ -103,7 +108,7 @@ module ActiveRecord # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>take!</tt> accepts no arguments. def take! - take or raise RecordNotFound + take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the first record (or first N records if a parameter is supplied). @@ -138,7 +143,7 @@ module ActiveRecord # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>first!</tt> accepts no arguments. def first! - first or raise RecordNotFound + find_nth! 0 end # Find the last record (or last N records if a parameter is supplied). @@ -171,7 +176,7 @@ module ActiveRecord # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>last!</tt> accepts no arguments. def last! - last or raise RecordNotFound + last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the second record. @@ -187,7 +192,7 @@ module ActiveRecord # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def second! - second or raise RecordNotFound + find_nth! 1 end # Find the third record. @@ -203,7 +208,7 @@ module ActiveRecord # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def third! - third or raise RecordNotFound + find_nth! 2 end # Find the fourth record. @@ -219,7 +224,7 @@ module ActiveRecord # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fourth! - fourth or raise RecordNotFound + find_nth! 3 end # Find the fifth record. @@ -235,7 +240,7 @@ module ActiveRecord # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fifth! - fifth or raise RecordNotFound + find_nth! 4 end # Find the forty-second record. Also known as accessing "the reddit". @@ -251,7 +256,7 @@ module ActiveRecord # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def forty_two! - forty_two or raise RecordNotFound + find_nth! 41 end # Returns +true+ if a record exists in the table that matches the +id+ or @@ -284,8 +289,10 @@ module ActiveRecord def exists?(conditions = :none) if Base === conditions conditions = conditions.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `exists?`. + Please pass the id of the object by calling `.id` + MSG end return false if !conditions @@ -300,11 +307,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 @@ -316,7 +323,7 @@ module ActiveRecord # the expected number of results should be provided in the +expected_size+ # argument. def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc: - conditions = arel.where_sql + conditions = arel.where_sql(@klass.arel_engine) conditions = " [#{conditions}]" if conditions if Array(ids).size == 1 @@ -358,7 +365,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 @@ -390,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end @@ -403,12 +410,12 @@ 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 def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } + reflections.none?(&:collection?) end protected @@ -430,19 +437,20 @@ module ActiveRecord else find_some(ids) end + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" end def find_one(id) if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, id]] + relation = where(primary_key => id) record = relation.take raise_record_not_found_exception!(id, 0, 1) unless record @@ -451,7 +459,7 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).to_a + result = where(primary_key => ids).to_a expected_size = if limit_value && ids.size > limit_value @@ -489,6 +497,10 @@ module ActiveRecord end end + def find_nth!(index) + find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + def find_nth_with_limit(offset, limit) relation = if order_values.empty? && primary_key order(arel_table[primary_key].asc) 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 ac41d0aa80..65b607ff1c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -13,7 +13,7 @@ module ActiveRecord @hash = hash end - def merge + def merge #:nodoc: Merger.new(relation, other).merge end @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v @@ -49,9 +49,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 - + [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -75,6 +75,7 @@ module ActiveRecord merge_multi_values merge_single_values + merge_clauses merge_joins relation @@ -83,12 +84,12 @@ module ActiveRecord private def merge_joins - return if values[:joins].blank? + return if other.joins_values.blank? if other.klass == relation.klass - relation.joins!(*values[:joins]) + relation.joins!(*other.joins_values) else - joins_dependency, rest = values[:joins].partition do |join| + joins_dependency, rest = other.joins_values.partition do |join| case join when Hash, Symbol, Array true @@ -107,74 +108,30 @@ module ActiveRecord end def merge_multi_values - lhs_wheres = relation.where_values - rhs_wheres = values[:where] || [] - - lhs_binds = relation.bind_values - rhs_binds = values[:bind] || [] - - removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - - where_values = kept + rhs_wheres - bind_values = filter_binds(lhs_binds, removed) + rhs_binds - - conn = relation.klass.connection - bv_index = 0 - where_values.map! do |node| - if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right - substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) - bv_index += 1 - Arel::Nodes::Equality.new(node.left, substitute) - else - node - end - end - - relation.where_values = where_values - relation.bind_values = bind_values - - if values[:reordering] + if other.reordering_value # override any order specified in the original relation - relation.reorder! values[:order] - elsif values[:order] + relation.reorder! other.order_values + elsif other.order_values # merge in order_values from relation - relation.order! values[:order] + relation.order! other.order_values end - relation.extend(*values[:extending]) unless values[:extending].blank? + relation.extend(*other.extending_values) unless other.extending_values.blank? end def merge_single_values - relation.from_value = values[:from] unless relation.from_value - relation.lock_value = values[:lock] unless relation.lock_value + relation.lock_value ||= other.lock_value - unless values[:create_with].blank? - relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + 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 eff5c8f09c..43e9afe853 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,82 +1,49 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] - - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' - - def self.resolve_column_aliases(klass, hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' + + delegate :resolve_column_aliases, to: :table + + def initialize(table) + @table = table + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end - def self.build_from_hash(klass, attributes, default_table) - queries = [] - - attributes.each do |column, value| - table = default_table - - if value.is_a?(Hash) - if value.empty? - queries << '1=0' - else - table = Arel::Table.new(column, default_table.engine) - association = klass._reflect_on_association(column.to_sym) - - value.each do |k, v| - queries.concat expand(association && association.klass, table, k, v) - end - end - else - column = column.to_s - - if column.include?('.') - table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, default_table.engine) - end - - queries.concat expand(klass, table, column, value) - end - end - - queries + def build_from_hash(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + expand_from_hash(attributes) end - def self.expand(klass, table, column, value) - queries = [] + 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) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column.to_sym) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << build(table[reflection.foreign_type], base_class) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << build(table[column], value) - queries - end - - def self.polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -101,26 +68,82 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - # FIXME: I think we need to deprecate this behavior - register_handler(Class, ->(attribute, value) { attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.in(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - private_class_method :build - def self.handler_for(object) + protected + + attr_reader :table + + def expand_from_hash(attributes) + return ["1=0"] if attributes.empty? + + attributes.flat_map do |key, value| + if value.is_a?(Hash) + 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?(".") } + + dot_notation.each do |key| + table_name, column_name = key.split(".") + value = attributes.delete(key) + attributes[table_name] ||= {} + + attributes[table_name] = attributes[table_name].merge(column_name => value) + end + + attributes + end + + def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end - private_class_method :handler_for + + 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/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index b8d9240bf8..95dbd6a77f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,17 +1,14 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) - if values.any? { |val| val.is_a?(Array) } - ActiveSupport::Deprecation.warn "Passing a nested array to Active Record " \ - "finder methods is deprecated and will be removed. Flatten your array " \ - "before using it for 'IN' conditions." - values = values.flatten - end - return attribute.in([]) if values.empty? && nils.empty? ranges, values = values.partition { |v| v.is_a?(Range) } @@ -19,20 +16,24 @@ module ActiveRecord values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) + when 1 then predicate_builder.build(attribute, values.first) else attribute.in(values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other 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 new file mode 100644 index 0000000000..159889d3b8 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,78 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + case value + when Relation + value.select(primary_key) + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def primary_key + associated_table.association_primary_key(base_class) + end + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..6cec75dc0a --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..1b3849e3ad --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 618fa3cdd9..063150958a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -6,7 +6,7 @@ module ActiveRecord value = value.select(value.klass.arel_table[value.klass.primary_key]) end - attribute.in(value.arel.ast) + attribute.in(value.arel) 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 bbddd28ccc..69ce5cdc2a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,5 +1,9 @@ -require 'active_support/core_ext/array/wrap' +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 @@ -38,38 +42,24 @@ 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 Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded - check_cached_relation - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + assert_mutability! # assert_mutability! + @values[:#{name}] = values # @values[:select] = values + end # end CODE end @@ -84,19 +74,27 @@ module ActiveRecord Relation::SINGLE_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) - raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded - check_cached_relation + assert_mutability! # assert_mutability! @values[:#{name}] = value # @values[:readonly] = value end # end CODE end - def check_cached_relation # :nodoc: - if defined?(@arel) && @arel - @arel = nil - ActiveSupport::Deprecation.warn "Modifying already cached Relation. The " \ - "cache will be reset. Use a cloned Relation to prevent this warning." - 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: @@ -401,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." @@ -424,27 +421,16 @@ module ActiveRecord # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) - - args.compact! - args.flatten! - spawn.joins!(*args) end def joins!(*args) # :nodoc: + args.compact! + args.flatten! self.joins_values += args self end - def bind(value) - 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. # @@ -580,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 @@ -596,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 +uniq+ 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. # @@ -607,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 @@ -755,7 +772,7 @@ module ActiveRecord end def from!(value, subquery_name = nil) # :nodoc: - self.from_value = [value, subquery_name] + self.from_clause = Relation::FromClause.new(value, subquery_name) self end @@ -856,35 +873,30 @@ module ActiveRecord private + def assert_mutability! + raise ImmutableRelation if @loaded + raise ImmutableRelation if defined?(@arel) && @arel + end + def build_arel - arel = Arel::SelectManager.new(table.engine, table) + arel = Arel::SelectManager.new(table) 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 - # Reorder bind indexes if joins produced bind values - bvs = arel.bind_values + bind_values - arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| - column = bvs[i].first - bp.replace connection.substitute_at(column, i) - end - arel end @@ -893,110 +905,36 @@ 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::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual - subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name == 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 = PredicateBuilder.resolve_column_aliases(klass, opts) - - bv_len = bind_values.length - tmp_opts, bind_values = create_binds(opts, bv_len) - self.bind_values += bind_values - - attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - add_relations_to_bind_values(attributes) - - PredicateBuilder.build_from_hash(klass, attributes, table) - else - [opts] - end - end - - def create_binds(opts, idx) - 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 - - new_opts = {} - binds = [] - - bindable.each_with_index do |(column,value), index| - binds.push [@klass.columns_hash[column.to_s], value] - new_opts[column] = connection.substitute_at(column, index + idx) - end - - non_binds.each { |column,value| new_opts[column] = value } - - [new_opts, binds] + def association_for_table(table_name) + table_name = table_name.to_s + @klass._reflect_on_association(table_name) || + @klass._reflect_on_association(table_name.singularize) end def build_from - opts, name = from_value + opts = from_clause.value + name = from_clause.name case opts when Relation name ||= 'subquery' - self.bind_values = opts.bind_values + self.bind_values opts.arel.as(name.to_s) else opts @@ -1018,13 +956,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, @@ -1044,17 +983,35 @@ module ActiveRecord manager end - def build_select(arel, selects) - if !selects.empty? - expanded_select = selects.map do |field| - columns_hash.key?(field.to_s) ? arel_table[field] : field - end - arel.project(*expanded_select) + def convert_join_strings_to_ast(table, joins) + joins + .flatten + .reject(&:blank?) + .map { |join| table.create_string_join(Arel.sql(join)) } + end + + 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) + if from_clause.value + columns + else + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) + arel_table[field] + else + field + end + end + end + end + def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? @@ -1073,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?) @@ -1144,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/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..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,16 +58,13 @@ 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 private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb new file mode 100644 index 0000000000..f9b9e640ec --- /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::LessThanOrEqual, 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 8405fdaeb9..500c478e65 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,6 +42,10 @@ module ActiveRecord @column_types = column_types end + def length + @rows.length + end + def each if block_given? hash_rows.each { |row| yield row } @@ -77,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 ff70cbed0f..c7f55ebaa1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,14 +3,11 @@ 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 @@ -72,38 +69,14 @@ module ActiveRecord expanded_attrs end - # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { name: "foo'bar", group_id: 4 } - # # => "name='foo''bar' and group_id= 4" - # { status: nil, group_id: [1,2,3] } - # # => "status IS NULL and group_id IN (1,2,3)" - # { age: 13..18 } - # # => "age BETWEEN 13 AND 18" - # { 'other_records.id' => 7 } - # # => "`other_records`.`id` = 7" - # { other_records: { id: 7 } } - # # => "`other_records`.`id` = 7" - # And for value objects on a composed_of relationship: - # { address: Address.new("123 abc st.", "chicago") } - # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = PredicateBuilder.resolve_column_aliases self, attrs - attrs = expand_hash_conditions_for_aggregates(attrs) - - table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) - PredicateBuilder.build_from_hash(self, attrs, table).map { |b| - connection.visitor.compile b - }.join(' AND ') - end - alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" + value = type_for_attribute(attr.to_s).serialize(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end @@ -134,7 +107,7 @@ module ActiveRecord raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection - statement.gsub('?') do + statement.gsub(/\?/) do replace_bind_variable(bound.shift, c) end end @@ -159,10 +132,8 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection, column = nil) #:nodoc: - if column - c.quote(value, column) - elsif value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else @@ -182,7 +153,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 68d976c240..da95920571 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,6 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @types = @connection.native_database_types @version = Migrator::current_version rescue nil @options = options end @@ -100,7 +99,7 @@ HEADER # dump foreign keys at the end to make sure all dependent tables exist. if @connection.supports_foreign_keys? sorted_tables.each do |tbl| - foreign_keys(tbl, stream) + foreign_keys(tbl, stream) unless ignored?(tbl) end end end @@ -118,21 +117,24 @@ HEADER if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") - elsif pkcol.sql_type == 'uuid' - tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + end + pkcolspec = @connection.column_spec_for_primary_key(pkcol) + if pkcolspec + pkcolspec.each do |key, value| + tbl.print ", #{key}: #{value}" + end end else tbl.print ", id: false" end - tbl.print ", force: true" + tbl.print ", force: :cascade" tbl.puts " do |t|" # then dump all non-primary key columns column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - @connection.column_spec(column, @types) + @connection.column_spec(column) end.compact # find all migration keys used in this table @@ -242,12 +244,7 @@ HEADER def ignored?(table_name) ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(table_name) == ignored - when Regexp; remove_prefix_and_suffix(table_name) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + ignored === remove_prefix_and_suffix(table_name) end end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 3e43591672..fca4f1c9d3 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,11 @@ 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 end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 49cadb66d0..43c7b1c574 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,7 +30,13 @@ module ActiveRecord end def default_scoped # :nodoc: - relation.merge(build_default_scope) + scope = build_default_scope + + if scope + relation.spawn.merge!(scope) + else + relation + end end # Collects attributes from scopes that should be applied when creating @@ -139,6 +145,10 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles def scope(name, body, &block) + unless body.respond_to?(:call) + raise ArgumentError, 'The scope body needs to be callable.' + end + if dangerous_class_method?(name) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but Active Record already defined " \ @@ -147,11 +157,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 new file mode 100644 index 0000000000..a3023a0cb4 --- /dev/null +++ b/activerecord/lib/active_record/secure_token.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module SecureToken + extend ActiveSupport::Concern + + module ClassMethods + # Example using has_secure_token + # + # # Schema: User(token:string, auth_token:string) + # class User < ActiveRecord::Base + # has_secure_token + # has_secure_token :auth_token + # end + # + # user = User.new + # user.save + # user.token # => "4kUgL2pdQMSCQtjE" + # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7" + # user.regenerate_token # => true + # user.regenerate_auth_token # => true + # + # 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 + # <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_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 + SecureRandom.base58(24) + end + end + end +end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index bd9079b596..48c12dcf9f 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -11,7 +11,7 @@ module ActiveRecord #:nodoc: def serializable_hash(options = nil) options = options.try(:clone) || {} - options[:except] = Array(options[:except]).map { |n| n.to_s } + options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) super(options) 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 aece446384..95986c820c 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -1,22 +1,33 @@ module ActiveRecord # Statement cache is used to cache a single statement in order to avoid creating the AST again. - # Initializing the cache is done by passing the statement in the initialization block: + # Initializing the cache is done by passing the statement in the create block: # - # cache = ActiveRecord::StatementCache.new do - # Book.where(name: "my book").limit(100) + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: "my book").where("author_id > 3") # end # # The cached statement is executed by using the +execute+ method: # - # cache.execute + # cache.execute([], Book, Book.connection) # # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. # Database is queried when +to_a+ is called on the relation. - class StatementCache - class Substitute; end + # + # If you want to cache the statement without the values you can use the +bind+ method of the + # block parameter. + # + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: params.bind) + # end + # + # And pass the bind values as the first argument of +execute+ call. + # + # cache.execute(["my book"], Book, Book.connection) + class StatementCache # :nodoc: + class Substitute; end # :nodoc: - class Query + class Query # :nodoc: def initialize(sql) @sql = sql end @@ -26,7 +37,7 @@ module ActiveRecord end end - class PartialQuery < Query + class PartialQuery < Query # :nodoc: def initialize values @values = values @indexes = values.each_with_index.find_all { |thing,i| @@ -36,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 @@ -51,26 +62,26 @@ module ActiveRecord PartialQuery.new collected end - class Params + class Params # :nodoc: def bind; Substitute.new; end end - class BindMap - def initialize(bind_values) + class BindMap # :nodoc: + 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 { |pair| pair.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 @@ -78,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/store.rb b/activerecord/lib/active_record/store.rb index 3c291f28e3..919bc58ba5 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,11 +15,15 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for # the serialization provided by +store+. Simply use +store_accessor+ instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. # + # NOTE: The default validations with the exception of +uniqueness+ will work. + # For example, if you want to check for +uniqueness+ with +hstore+ you will + # need to use a custom validation to handle it. + # # Examples: # # class User < ActiveRecord::Base diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb new file mode 100644 index 0000000000..b0b86865fd --- /dev/null +++ b/activerecord/lib/active_record/suppressor.rb @@ -0,0 +1,55 @@ +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 + + # Ignore saving events if we're in suppression mode. + def save!(*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 new file mode 100644 index 0000000000..3dd6321a97 --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,62 @@ +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 + @arel_table = arel_table + @association = association + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + 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 + + def associated_table(table_name) + return self if table_name == arel_table.name + + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + arel_table = association_klass.arel_table + else + type_caster = TypeCaster::Connection.new(klass, table_name) + association_klass = nil + arel_table = Arel::Table.new(table_name, type_caster: type_caster) + end + + TableMetadata.new(association_klass, arel_table, association) + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end +end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index f9b54139d5..69aceb66b1 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -186,37 +188,48 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end - def load_schema(format = ActiveRecord::Base.schema_format, file = nil) - ActiveSupport::Deprecation.warn \ - "This method will act on a specific connection in the future. " \ - "To act on the current connection, use `load_schema_current` instead." - - load_schema_current(format, file) - end + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: + file ||= schema_file(format) - # This method is the successor of +load_schema+. We should rename it - # after +load_schema+ went through a deprecation cycle. (Rails > 4.2) - def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: case format when :ruby - file ||= File.join(db_dir, "schema.rb") check_schema_file(file) - purge(configuration) ActiveRecord::Base.establish_connection(configuration) load(file) when :sql - file ||= File.join(db_dir, "structure.sql") check_schema_file(file) - purge(configuration) structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_for(*args) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + This method was renamed to `#load_schema` and will be removed in the future. + Use `#load_schema` instead. + MSG + load_schema(*args) + end + + def schema_file(format = ActiveSupport::Base.schema_format) + case format + when :ruby + File.join(db_dir, "schema.rb") + when :sql + File.join(db_dir, "structure.sql") + end + end + + def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + if File.exist?(file || schema_file(format)) + load_schema_current(format, file, environment) + end + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| - load_schema_for configuration, format, file + load_schema configuration, format, file } ActiveRecord::Base.establish_connection(environment.to_sym) end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index d890196f47..eafbb2c249 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -31,6 +31,7 @@ module ActiveRecord end establish_connection configuration else + $stderr.puts error.inspect $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 936a18d99a..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 45bc10b9b0..598defce50 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -2,24 +2,16 @@ module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern + #:nodoc: ACTIONS = [:create, :destroy, :update] - CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \ - "within `after_rollback`/`after_commit` callbacks and only print them to " \ - "the logs. In the next version, these errors will no longer be suppressed. " \ - "Instead, the errors will propagate normally just like in other Active " \ - "Record callbacks.\n" \ - "\n" \ - "You can opt into the new behavior and remove this warning by setting:\n" \ - "\n" \ - " config.active_record.raise_in_transactional_callbacks = true" included do define_callbacks :commit, :rollback, - terminator: ->(_, result) { result == false }, + :before_commit, + :before_commit_without_transaction_enrollment, + :commit_without_transaction_enrollment, + :rollback_without_transaction_enrollment, scope: [:kind, :name] - - mattr_accessor :raise_in_transactional_callbacks, instance_writer: false - self.raise_in_transactional_callbacks = false end # = Active Record Transactions @@ -218,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 @@ -235,9 +232,6 @@ module ActiveRecord def after_commit(*args, &block) set_options_for_callbacks!(args) set_callback(:commit, :after, *args, &block) - unless ActiveRecord::Base.raise_in_transactional_callbacks - ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE) - end end # This callback is called after a create, update, or destroy are rolled back. @@ -246,9 +240,31 @@ module ActiveRecord def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) - unless ActiveRecord::Base.raise_in_transactional_callbacks - ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE) - end + 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 + end + + def raise_in_transactional_callbacks=(value) + ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.') + value end private @@ -265,7 +281,7 @@ module ActiveRecord def assert_valid_transaction_action(actions) if (actions - ACTIONS).any? - raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" + raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}" end end end @@ -304,20 +320,31 @@ module ActiveRecord clear_transaction_record_state end + def before_committed! # :nodoc: + _run_before_commit_without_transaction_enrollment_callbacks + _run_before_commit_callbacks + 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_callbacks :commit if should_run_callbacks && destroyed? || persisted? + def committed!(should_run_callbacks: true) #:nodoc: + if should_run_callbacks && destroyed? || persisted? + _run_commit_without_transaction_enrollment_callbacks + _run_commit_callbacks + 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_callbacks :rollback if should_run_callbacks + def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: + if should_run_callbacks + _run_rollback_without_transaction_enrollment_callbacks + _run_rollback_callbacks + end ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state @@ -326,9 +353,12 @@ 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 + 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 @@ -358,14 +388,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. @@ -388,7 +416,10 @@ module ActiveRecord thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - write_attribute(self.class.primary_key, restore_state[:id]) + pk = self.class.primary_key + if pk && read_attribute(pk) != restore_state[:id] + write_attribute(pk, restore_state[:id]) + end end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index f1384e0bb2..2c0cda69d0 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,8 +1,7 @@ -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' require 'active_record/type/binary' require 'active_record/type/boolean' require 'active_record/type/date' @@ -15,6 +14,53 @@ require 'active_record/type/serialized' require 'active_record/type/string' 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/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb new file mode 100644 index 0000000000..0c72d8914f --- /dev/null +++ b/activerecord/lib/active_record/type/big_integer.rb @@ -0,0 +1,13 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +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/boolean.rb b/activerecord/lib/active_record/type/boolean.rb index 06dd17ed28..f6a75512fd 100644 --- a/activerecord/lib/active_record/type/boolean.rb +++ b/activerecord/lib/active_record/type/boolean.rb @@ -11,7 +11,7 @@ module ActiveRecord if value == '' nil else - ConnectionAdapters::Column::TRUE_VALUES.include?(value) + !ConnectionAdapters::Column::FALSE_VALUES.include?(value) end end 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 5f19608a33..a5199959b9 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,22 +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) - value.send(zone_conversion_method) - else - super - end - end - private def cast_value(string) @@ -38,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 d10778eeb6..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 @@ -16,7 +16,7 @@ module ActiveRecord def cast_value(value) case value when ::Float - BigDecimal(value, float_precision) + convert_float_to_big_decimal(value) when ::Numeric, ::String BigDecimal(value, precision.to_i) else @@ -28,6 +28,14 @@ module ActiveRecord end end + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else + value.to_d + end + end + def float_precision if precision.to_i > ::Float::DIG + 1 ::Float::DIG + 1 diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb index cabdcecdd7..ff5559e300 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -1,8 +1,8 @@ -require 'active_record/type/integer' +require 'active_record/type/big_integer' module ActiveRecord module Type - class DecimalWithoutScale < Integer # :nodoc: + class DecimalWithoutScale < BigInteger # :nodoc: def type :decimal 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 bf92680268..82d9327fc0 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -3,16 +3,14 @@ module ActiveRecord class HashLookupTypeMap < TypeMap # :nodoc: delegate :key?, to: :@mapping - def lookup(type, *args) - @mapping.fetch(type, proc { default_value }).call(type, *args) + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } end - def fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end + private - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) end end end 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 08477d1303..2a1b04ac7f 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -1,13 +1,37 @@ 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 + @range = min_value...max_value + end def type :integer end - alias type_cast_for_database type_cast + 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 private @@ -15,9 +39,25 @@ module ActiveRecord case value when true then 1 when false then 0 - else value.to_i rescue nil + else + 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 || DEFAULT_LIMIT}" end end + + def max_value + limit = self.limit || DEFAULT_LIMIT + 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + 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 fa43266504..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 5b512433b0..732029c723 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < SimpleDelegator # :nodoc: - include Mutable + class Serialized < DelegateClass(Type::Value) # :nodoc: + include Helpers::Mutable attr_reader :subtype, :coder @@ -11,7 +11,7 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) + def deserialize(value) if default_value?(value) value else @@ -19,7 +19,7 @@ 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) @@ -28,24 +28,13 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, coder.dump(value)) + subtype.changed_in_place?(raw_old_value, serialize(value)) end def accessor ActiveRecord::Store::IndifferentHashAccessor end - def init_with(coder) - @subtype = coder['subtype'] - @coder = coder['coder'] - __setobj__(@subtype) - end - - def encode_with(coder) - coder['subtype'] = @subtype - coder['coder'] = @coder - 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 150defb106..2662b7e874 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -11,12 +11,12 @@ 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) - when true then "1" - when false then "0" + when true then "t" + when false then "f" else super end end @@ -25,8 +25,8 @@ module ActiveRecord def cast_value(value) case value - when true then "1" - when false then "0" + when true then "t" + when false then "f" # String.new is slightly faster than dup else ::String.new(value.to_s) end 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/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 88c5f9c497..09f5ba6b74 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,24 +1,28 @@ +require 'thread_safe' + module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} + @cache = ThreadSafe::Cache.new do |h, key| + h.fetch_or_store(key, ThreadSafe::Cache.new) + end end def lookup(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end + fetch(lookup_key, *args) { default_value } + end - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - default_value + def fetch(lookup_key, *args, &block) + @cache[lookup_key].fetch_or_store(args) do + perform_fetch(lookup_key, *args, &block) end end def register_type(key, value = nil, &block) raise ::ArgumentError unless value || block + @cache.clear if block @mapping[key] = block @@ -40,6 +44,18 @@ module ActiveRecord private + def perform_fetch(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + yield lookup_key, *args + end + end + def default_value @default_value ||= Value.new end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb new file mode 100644 index 0000000000..ed3e527483 --- /dev/null +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + class UnsignedInteger < Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 9456a4a56c..fc3ef5e83b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -1,44 +1,46 @@ 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) + # Convert 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 # 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 @@ -50,17 +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 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. @@ -69,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 @@ -85,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.rb b/activerecord/lib/active_record/type_caster.rb new file mode 100644 index 0000000000..63ba10c289 --- /dev/null +++ b/activerecord/lib/active_record/type_caster.rb @@ -0,0 +1,7 @@ +require 'active_record/type_caster/map' +require 'active_record/type_caster/connection' + +module ActiveRecord + module TypeCaster + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb new file mode 100644 index 0000000000..3878270770 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -0,0 +1,29 @@ +module ActiveRecord + module TypeCaster + class 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) + column = column_for(attribute_name) + connection.type_cast_from_column(column, value) + end + + protected + + attr_reader :table_name + delegate :connection, to: :@klass + + private + + def column_for(attribute_name) + if connection.schema_cache.table_exists?(table_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb new file mode 100644 index 0000000000..4b1941351c --- /dev/null +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module TypeCaster + class Map + def initialize(types) + @types = types + end + + 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.serialize(value) + end + + protected + + attr_reader :types + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 7f7d49cdb4..e227212827 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -5,13 +5,14 @@ module ActiveRecord # +record+ method to retrieve the record which did not validate. # # begin - # complex_operation_that_calls_save!_internally + # complex_operation_that_internally_calls_save! # rescue ActiveRecord::RecordInvalid => invalid # puts invalid.record.errors # end class RecordInvalid < ActiveRecordError - attr_reader :record # :nodoc: - def initialize(record) # :nodoc: + attr_reader :record + + def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) @@ -39,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 @@ -60,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 @@ -87,3 +76,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b4785d3ba4..47ccef31a5 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -29,9 +29,11 @@ module ActiveRecord # Configuration options: # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb new file mode 100644 index 0000000000..5991fbad8e --- /dev/null +++ b/activerecord/lib/active_record/validations/length.rb @@ -0,0 +1,33 @@ +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 + # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + def validates_length_of(*attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) + end + + alias_method :validates_size_of, :validates_length_of + end + end +end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index e586744818..75d5bd5a35 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,13 +2,14 @@ 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) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. - if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? } + if associated_records.present? && associated_records.all?(&:marked_for_destruction?) record.errors.add(attribute, :blank, options) end end @@ -44,9 +45,11 @@ module ActiveRecord # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if # the validation should occur (e.g. <tt>if: :allow_validation</tt>, or # <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2dba4c7b94..9be4b10a55 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,14 +11,14 @@ 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) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -60,17 +60,22 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.connection.type_cast(value, column) + 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] end - if !options[:case_sensitive] && value.is_a?(String) + value = Arel::Nodes::Quoted.new(value) + + 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) end def scope_relation(record, table, relation) @@ -79,9 +84,9 @@ module ActiveRecord scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else - scope_value = record.read_attribute(scope_item) + scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation 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 f7bf6987c4..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,14 +4,19 @@ 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 -%> <% end -%> <% if options[:timestamps] %> - t.timestamps null: false + 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 6f84bae432..ecf1368a91 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) @@ -213,6 +213,16 @@ module ActiveRecord test "type_to_sql returns a String for unmapped types" do assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end + + unless current_adapter?(:PostgreSQLAdapter) + def test_log_invalid_encoding + assert_raise ActiveRecord::StatementInvalid do + @connection.send :log, "SELECT 'ы' FROM DUAL" do + raise 'ы'.force_encoding(Encoding::ASCII_8BIT) + end + end + end + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index a84673e452..6577d56240 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -92,7 +92,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :delete_me + ActiveRecord::Base.connection.add_timestamps :delete_me, null: true assert column_present?('delete_me', 'updated_at', 'datetime') assert column_present?('delete_me', 'created_at', 'datetime') ensure @@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase ActiveRecord::Base.connection.create_table :delete_me do |t| t.timestamps null: true end - ActiveRecord::Base.connection.remove_timestamps :delete_me + ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true } assert !column_present?('delete_me', 'updated_at', 'datetime') assert !column_present?('delete_me', 'created_at', 'datetime') ensure diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 3dabb1104a..4762ef43b5 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -47,9 +47,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert !@connection.active? # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect @@ -69,8 +67,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase end def test_bind_value_substitute - bind_param = @connection.substitute_at('foo', 0) - assert_equal Arel.sql('?'), bind_param + bind_param = @connection.substitute_at('foo') + assert_equal Arel.sql('?'), bind_param.to_sql end def test_exec_no_binds @@ -96,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 @@ -108,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 diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 28106d3772..48ceef365e 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'support/ddl_helper' @@ -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 @@ -99,13 +98,19 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do + assert_nil @conn.primary_key('ex') + end + end + def test_tinyint_integer_typecasting with_example_table '`status` TINYINT(4)' do insert(@conn, { 'status' => 2 }, 'ex') 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 @@ -123,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 d8a954efa8..a2206153e9 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,15 +9,11 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 1, @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 0, @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) + assert_equal 0, @conn.type_cast(false) end 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 61ae0abfd1..2f9c070255 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -101,7 +101,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 - assert(gs.collect{|x| x.id}.sort == [2, 3]) + assert(gs.collect(&:id).sort == [2, 3]) end # has_and_belongs_to_many with reserved-word table name @@ -110,7 +110,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 - assert(s.collect{|x|x.id}.sort == [1, 2]) + assert(s.collect(&:id).sort == [1, 2]) end # activerecord model introspection with reserved-word table and column names @@ -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 87c5277e64..b7f9c2ce84 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -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 @@ -81,7 +81,7 @@ module ActiveRecord table = 'key_tests' - indexes = @connection.indexes(table).sort_by {|i| i.name} + indexes = @connection.indexes(table).sort_by(&:name) assert_equal 3,indexes.size index_a = indexes.select{|i| i.name == index_a_name}[0] diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb new file mode 100644 index 0000000000..8f521e9181 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class UnsignedTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class UnsignedType < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("unsigned_types", force: true) do |t| + t.column :unsigned_integer, "int unsigned" + end + end + + teardown do + @connection.drop_table "unsigned_types" + end + + test "unsigned int max value is in range" do + assert expected = UnsignedType.create(unsigned_integer: 4294967295) + assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295) + end + + test "minus value is out of range" do + assert_raise(RangeError) do + UnsignedType.create(unsigned_integer: -10) + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 49cfafd7a5..e87cd3886a 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -92,7 +92,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :delete_me + ActiveRecord::Base.connection.add_timestamps :delete_me, null: true assert column_present?('delete_me', 'updated_at', 'datetime') assert column_present?('delete_me', 'created_at', 'datetime') ensure @@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase ActiveRecord::Base.connection.create_table :delete_me do |t| t.timestamps null: true end - ActiveRecord::Base.connection.remove_timestamps :delete_me + ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true } assert !column_present?('delete_me', 'updated_at', 'datetime') assert !column_present?('delete_me', 'created_at', 'datetime') ensure diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 03627135b2..0e641ba3bf 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,8 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal "1", @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "test type casting without emulated booleans" do @@ -60,8 +59,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal "1", @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 0b2f59b0eb..a8b39b21d4 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -4,6 +4,8 @@ require 'support/connection_helper' class MysqlConnectionTest < ActiveRecord::TestCase include ConnectionHelper + fixtures :comments + def setup super @subscriber = SQLSubscriber.new @@ -20,10 +22,21 @@ 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 + def test_truncate + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_operator count, :>, 0 + + ActiveRecord::Base.connection.truncate("comments") + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_equal 0, count + end + def test_no_automatic_reconnection_after_timeout assert @connection.active? @connection.update('set @@wait_timeout=1') @@ -31,9 +44,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert !@connection.active? # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect @@ -111,11 +122,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/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 675703caa1..2b01d941b8 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -17,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 799d927ee4..beb829fc46 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -100,7 +100,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 - assert(gs.collect{|x| x.id}.sort == [2, 3]) + assert(gs.collect(&:id).sort == [2, 3]) end # has_and_belongs_to_many with reserved-word table name @@ -109,7 +109,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 - assert(s.collect{|x|x.id}.sort == [1, 2]) + assert(s.collect(&:id).sort == [1, 2]) end # activerecord model introspection with reserved-word table and column names @@ -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..417ccf6d11 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -2,7 +2,7 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters - class Mysql2Adapter + class AbstractMysqlAdapter class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" @@ -16,23 +16,31 @@ module ActiveRecord def test_initializes_schema_migrations_for_encoding_utf8mb4 smtn = ActiveRecord::Migrator.schema_migrations_table_name - connection.drop_table(smtn) if connection.table_exists?(smtn) + connection.drop_table smtn, if_exists: true - config = connection.instance_variable_get(:@config) - original_encoding = config[:encoding] + 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") - config[:encoding] = 'utf8mb4' connection.initialize_schema_migrations_table - assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) ensure - config[:encoding] = original_encoding + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") end private def connection @connection ||= ActiveRecord::Base.connection end + + def execute(sql) + connection.execute(sql) + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 1b7e60565d..47707b7d4f 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -51,7 +51,7 @@ module ActiveRecord table = 'key_tests' - indexes = @connection.indexes(table).sort_by {|i| i.name} + indexes = @connection.indexes(table).sort_by(&:name) assert_equal 3,indexes.size index_a = indexes.select{|i| i.name == index_a_name}[0] diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb new file mode 100644 index 0000000000..8f521e9181 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class UnsignedTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class UnsignedType < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("unsigned_types", force: true) do |t| + t.column :unsigned_integer, "int unsigned" + end + end + + teardown do + @connection.drop_table "unsigned_types" + end + + test "unsigned int max value is in range" do + assert expected = UnsignedType.create(unsigned_integer: 4294967295) + assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295) + end + + test "minus value is out of range" do + assert_raise(RangeError) do + UnsignedType.create(unsigned_integer: -10) + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index ff553c3f1a..2163e35e70 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,7 +1,8 @@ -# encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlArrayTest < ActiveRecord::TestCase + include SchemaDumpingHelper include InTimeZone OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID @@ -23,24 +24,23 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end end @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 def test_column assert_equal :string, @column.type assert_equal "character varying", @column.sql_type - assert @column.array - assert_not @column.number? - assert_not @column.binary? + assert @column.array? + 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? + assert ratings_column.array? end def test_default @@ -72,7 +72,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :text, column.type assert_equal [], PgArray.column_defaults['snippets'] - assert column.array + assert column.array? end def test_change_column_cant_make_non_array_column_to_array @@ -92,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 @@ -108,6 +108,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal([1, 2], x.ratings) end + 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 + end + def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first @@ -200,7 +206,7 @@ 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 @@ -208,8 +214,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase 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 @@ -254,6 +260,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end end + def test_assigning_non_array_value + record = PgArray.new(tags: "not-an-array") + assert_equal [], record.tags + assert_equal "not-an-array", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_empty_string + record = PgArray.new(tags: "") + assert_equal [], record.tags + assert_equal "", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_valid_pg_array_literal + record = PgArray.new(tags: "{1,2,3}") + assert_equal ["1", "2", "3"], record.tags + assert_equal "{1,2,3}", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_uniqueness_validation + klass = Class.new(PgArray) do + validates_uniqueness_of :tags + + def self.model_name; ActiveModel::Name.new(PgArray) end + end + e1 = klass.create("tags" => ["black", "blue"]) + assert e1.persisted?, "Saving e1" + + e2 = klass.create("tags" => ["black", "blue"]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:tags].any?, "Should have errors for tags" + assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags" + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 72222c01fd..1a5ff4316c 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' @@ -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 + 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 + 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 7872f91943..16db5ab83d 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" class PostgresqlByteaTest < ActiveRecord::TestCase @@ -17,32 +16,40 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end end @column = ByteaDataType.columns_hash['payload'] - assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) + @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 + assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn) assert_equal :binary, @column.type end + def test_binary_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal 'bytea', @connection.type_to_sql(:binary, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql :binary, 4294967295 + end + end + def test_type_cast_binary_converts_the_encoding assert @column 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 new file mode 100644 index 0000000000..5a9796887c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -0,0 +1,38 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PGChangeSchemaTest < ActiveRecord::TestCase + attr_reader :connection + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table(:strings) do |t| + t.string :somedate + end + end + + def teardown + connection.drop_table :strings + end + + def test_change_string_to_date + connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)' + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type + end + + def test_change_type_with_symbol + 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 new file mode 100644 index 0000000000..6cb11d17b4 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "ipaddr" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class CidrTest < ActiveRecord::TestCase + 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.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.serialize("foo") + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index cb024463c9..f706847890 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,8 +1,9 @@ -# encoding: utf-8 require 'cases/helper' +require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? class PostgresqlCitextTest < ActiveRecord::TestCase + include SchemaDumpingHelper class Citext < ActiveRecord::Base self.table_name = 'citexts' end @@ -18,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 @@ -30,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 + assert_not column.array? + + type = Citext.type_for_attribute('cival') + assert_not type.binary? end def test_change_table_supports_json @@ -67,5 +69,10 @@ if ActiveRecord::Base.connection.supports_extensions? x = Citext.where(cival: 'cased text').first assert_equal 'Cased Text', x.cival end + + def test_schema_dump_with_shorthand + output = dump_table_schema("citexts") + assert_match %r[t\.citext "cival"], output + end end end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index cfab5ca902..16e3f90a47 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 @@ -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 + assert_not column.array? + + type = PostgresqlComposite.type_for_attribute("address") + assert_not type.binary? end def test_composite_mapping @@ -83,17 +83,17 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase 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 + 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 d26cda46fa..55ad76c8c0 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -8,6 +8,8 @@ module ActiveRecord class NonExistentTable < ActiveRecord::Base end + fixtures :comments + def setup super @subscriber = SQLSubscriber.new @@ -20,6 +22,14 @@ module ActiveRecord super end + def test_truncate + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i + assert_operator count, :>, 0 + ActiveRecord::Base.connection.truncate("comments") + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i + assert_equal 0, count + end + def test_encoding assert_not_nil @connection.encoding end @@ -116,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 @@ -167,9 +177,7 @@ module ActiveRecord "successfully querying with the same connection pid." # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_set_session_variable_true diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index a0a34e4b87..4f48a7bce3 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -2,9 +2,6 @@ require "cases/helper" require 'support/ddl_helper' -class PostgresqlNumber < ActiveRecord::Base -end - class PostgresqlTime < ActiveRecord::Base end @@ -20,13 +17,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") - @first_number = PostgresqlNumber.find(1) - @second_number = PostgresqlNumber.find(2) - @third_number = PostgresqlNumber.find(3) - @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) @@ -35,12 +25,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end teardown do - [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all) - end - - def test_data_type_of_number_types - assert_equal :float, @first_number.column_for_attribute(:single).type - assert_equal :float, @first_number.column_for_attribute(:double).type + [PostgresqlTime, PostgresqlOid].each(&:delete_all) end def test_data_type_of_time_types @@ -52,14 +37,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end - def test_number_values - assert_equal 123.456, @first_number.single - assert_equal 123456.789, @first_number.double - assert_equal(-::Float::INFINITY, @second_number.single) - assert_equal ::Float::INFINITY, @second_number.double - assert_same ::Float::NAN, @third_number.double - end - def test_time_values assert_equal '-1 years -2 days', @first_time.time_interval assert_equal '-21 days', @first_time.scaled_time_interval @@ -69,17 +46,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal 1234, @first_oid.obj_id end - def test_update_number - new_single = 789.012 - new_double = 789012.345 - @first_number.single = new_single - @first_number.double = new_double - assert @first_number.save - assert @first_number.reload - assert_equal new_single, @first_number.single - assert_equal new_double, @first_number.double - end - def test_update_time @first_time.time_interval = '2 years 3 minutes' assert @first_time.save @@ -94,6 +60,13 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert @first_oid.reload assert_equal new_value, @first_oid.obj_id end + + def test_text_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal 'text', @connection.type_to_sql(:text, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql :text, 4294967295 + end + end end class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 1500adb42d..26e064c937 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' @@ -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 + 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 d99c4a292e..7458de23d8 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlEnumTest < ActiveRecord::TestCase include ConnectionHelper @@ -24,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 @@ -33,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 + assert_not column.array? + + type = PostgresqlEnum.type_for_attribute("current_mood") + assert_not type.binary? end def test_enum_defaults diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 19053b6732..6ffb4c9f33 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -17,7 +18,7 @@ module ActiveRecord 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" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index 9dadb177ca..b83063c94e 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,21 +1,34 @@ -# encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlFullTextTest < ActiveRecord::TestCase - class PostgresqlTsvector < ActiveRecord::Base; end + include SchemaDumpingHelper + class Tsvector < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('tsvectors') do |t| + t.tsvector 'text_vector' + end + end + + teardown do + @connection.drop_table 'tsvectors', if_exists: true + end def test_tsvector_column - column = PostgresqlTsvector.columns_hash["text_vector"] + 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 + assert_not column.array? + + type = Tsvector.type_for_attribute("text_vector") + assert_not type.binary? end def test_update_tsvector - PostgresqlTsvector.create text_vector: "'text' 'vector'" - tsvector = PostgresqlTsvector.first + Tsvector.create text_vector: "'text' 'vector'" + tsvector = Tsvector.first assert_equal "'text' 'vector'", tsvector.text_vector tsvector.text_vector = "'new' 'text' 'vector'" @@ -23,4 +36,9 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase assert tsvector.reload assert_equal "'new' 'text' 'vector'", tsvector.text_vector end + + def test_schema_dump_with_shorthand + output = dump_table_schema("tsvectors") + 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 6c0adbbeaa..41e9572907 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' @@ -11,26 +10,25 @@ class PostgresqlPointTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.transaction do - @connection.create_table('postgresql_points') do |t| - t.point :x - t.point :y, default: [12.2, 13.3] - t.point :z, default: "(14.4,15.5)" - end + @connection.create_table('postgresql_points') do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" end end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_points' + @connection.drop_table 'postgresql_points', if_exists: true 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 + assert_not column.array? + + type = PostgresqlPoint.type_for_attribute("x") + assert_not type.binary? end def test_default @@ -70,3 +68,72 @@ class PostgresqlPointTest < ActiveRecord::TestCase assert_not p.changed? end end + +class PostgresqlGeometricTest < ActiveRecord::TestCase + class PostgresqlGeometric < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_geometrics") do |t| + t.column :a_line_segment, :lseg + t.column :a_box, :box + t.column :a_path, :path + t.column :a_polygon, :polygon + t.column :a_circle, :circle + end + end + + teardown do + @connection.drop_table 'postgresql_geometrics', if_exists: true + end + + def test_geometric_types + g = PostgresqlGeometric.new( + :a_line_segment => '(2.0, 3), (5.5, 7.0)', + :a_box => '2.0, 3, 5.5, 7.0', + :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', + :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', + :a_circle => '<(5.3, 10.4), 2>' + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + end + + def test_alternative_format + g = PostgresqlGeometric.new( + :a_line_segment => '((2.0, 3), (5.5, 7.0))', + :a_box => '(2.0, 3), (5.5, 7.0)', + :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', + :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', + :a_circle => '((5.3, 10.4), 2)' + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + end + + def test_geometric_function + PostgresqlGeometric.create! a_path: '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]' # [ ] is an open path + PostgresqlGeometric.create! a_path: '((2.0, 3), (5.5, 7.0), (8.5, 11.0))' # ( ) is a closed path + + objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [true, false], objs.map(&:isopen) + + objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [false, true], objs.map(&:isclosed) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 1296eb72c0..e6835031c3 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,41 +1,40 @@ -# encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' -class PostgresqlHstoreTest < ActiveRecord::TestCase - class Hstore < ActiveRecord::Base - self.table_name = 'hstores' +if ActiveRecord::Base.connection.supports_extensions? + class PostgresqlHstoreTest < ActiveRecord::TestCase + include SchemaDumpingHelper + class Hstore < ActiveRecord::Base + self.table_name = 'hstores' - store_accessor :settings, :language, :timezone - end + store_accessor :settings, :language, :timezone + end - def setup - @connection = ActiveRecord::Base.connection + def setup + @connection = ActiveRecord::Base.connection - unless @connection.extension_enabled?('hstore') - @connection.enable_extension 'hstore' - @connection.commit_db_transaction - end + unless @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + @connection.commit_db_transaction + end - @connection.reconnect! + @connection.reconnect! - @connection.transaction do - @connection.create_table('hstores') do |t| - t.hstore 'tags', :default => '' - t.hstore 'payload', array: true - t.hstore 'settings' + @connection.transaction do + @connection.create_table('hstores') do |t| + t.hstore 'tags', :default => '' + t.hstore 'payload', array: true + t.hstore 'settings' + end end + @column = Hstore.columns_hash['tags'] + @type = Hstore.type_for_attribute("tags") end - @column = Hstore.columns_hash['tags'] - end - teardown do - @connection.execute 'drop table if exists hstores' - end + teardown do + @connection.drop_table 'hstores', if_exists: true + end - if ActiveRecord::Base.connection.supports_extensions? def test_hstore_included_in_extensions assert @connection.respond_to?(:extensions), "connection should have a list of extensions" assert @connection.extensions.include?('hstore'), "extension list should include hstore" @@ -55,9 +54,9 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase 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 @column.array? + + assert_not @type.binary? end def test_default @@ -111,10 +110,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase 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 @@ -166,47 +165,47 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase 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 @@ -315,10 +314,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase dupe = record.dup assert_equal({"one" => "two"}, dupe.tags.to_hash) end - end - private + def test_schema_dump_with_shorthand + output = dump_table_schema("hstores") + assert_match %r[t\.hstore "tags",\s+default: {}], output + end + private def assert_array_cycle(array) # test creation x = Hstore.create!(payload: array) @@ -346,4 +348,5 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase x.reload assert_equal(hash, x.tags) end + end end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 22e8873333..24199c69b8 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -1,6 +1,8 @@ require "cases/helper" class PostgresqlInfinityTest < ActiveRecord::TestCase + include InTimeZone + class PostgresqlInfinity < ActiveRecord::Base end @@ -13,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 @@ -41,4 +43,18 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase record.reload assert_equal Float::INFINITY, record.datetime end + + test "assigning 'infinity' on a datetime column with TZ aware attributes" do + begin + in_time_zone "Pacific Time (US & Canada)" do + record = PostgresqlInfinity.create!(datetime: "infinity") + assert_equal Float::INFINITY, record.datetime + assert_equal record.datetime, record.reload.datetime + end + ensure + # setting time_zone_aware_attributes causes the types to change. + # There is no way to do this automatically since it can be set on a superclass + PostgresqlInfinity.reset_column_information + end + end end diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb new file mode 100644 index 0000000000..679a0fc7b3 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "active_support/core_ext/numeric/bytes" + +class PostgresqlIntegerTest < ActiveRecord::TestCase + class PgInteger < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.create_table "pg_integers", force: true do |t| + t.integer :quota, limit: 8, default: 2.gigabytes + end + end + end + + teardown do + @connection.drop_table "pg_integers", if_exists: true + end + + test "schema properly respects bigint ranges" do + assert_equal 2.gigabytes, PgInteger.new.quota + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 86ba849445..d8fded16b4 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,10 +1,10 @@ -# encoding: utf-8 - +# -*- coding: utf-8 -*- require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' module PostgresqlJSONSharedTestCases + include SchemaDumpingHelper + class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' @@ -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 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 + assert_not column.array? + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? end def test_default @@ -64,6 +63,11 @@ module PostgresqlJSONSharedTestCases JsonDataType.reset_column_information end + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) + end + def test_cast_value_on_write x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) @@ -73,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 @@ -174,6 +178,14 @@ 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 diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 2968109346..ce0ad16557 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,8 @@ -# encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlLtreeTest < ActiveRecord::TestCase + include SchemaDumpingHelper class Ltree < ActiveRecord::Base self.table_name = 'ltrees' end @@ -21,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 + assert_not column.array? + + type = Ltree.type_for_attribute('path') + assert_not type.binary? end def test_write @@ -43,4 +45,9 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase ltree = Ltree.first assert_equal '1.2.3', ltree.path end + + def test_schema_dump_with_shorthand + output = dump_table_schema("ltrees") + 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 87183174f2..cedd399380 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'support/schema_dumping_helper' @@ -10,14 +9,14 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") - @connection.create_table('postgresql_moneys') do |t| - t.column "wealth", "money" - t.column "depth", "money", default: "150.55" + @connection.create_table('postgresql_moneys', force: true) do |t| + 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 + 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 4f4c1103fa..033695518e 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,35 +1,51 @@ -# encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlNetworkTest < ActiveRecord::TestCase - class PostgresqlNetworkAddress < ActiveRecord::Base + include SchemaDumpingHelper + class PostgresqlNetworkAddress < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_network_addresses', force: true) do |t| + t.inet 'inet_address', default: "192.168.1.1" + t.cidr 'cidr_address', default: "192.168.1.0/24" + t.macaddr 'mac_address', default: "ff:ff:ff:ff:ff:ff" + end + end + + teardown do + @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 + 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 + 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 + assert_not column.array? + + type = PostgresqlNetworkAddress.type_for_attribute("mac_address") + assert_not type.binary? end def test_network_types @@ -68,4 +84,11 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_nil invalid_address.cidr_address_before_type_cast assert_nil invalid_address.inet_address_before_type_cast end + + 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 + end end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb new file mode 100644 index 0000000000..093b81fe8d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -0,0 +1,49 @@ +require "cases/helper" + +class PostgresqlNumberTest < ActiveRecord::TestCase + class PostgresqlNumber < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_numbers', force: true) do |t| + t.column 'single', 'REAL' + t.column 'double', 'DOUBLE PRECISION' + end + end + + teardown do + @connection.drop_table 'postgresql_numbers', if_exists: true + end + + def test_data_type + assert_equal :float, PostgresqlNumber.columns_hash["single"].type + assert_equal :float, PostgresqlNumber.columns_hash["double"].type + end + + def test_values + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") + + first, second, third = PostgresqlNumber.find(1, 2, 3) + + assert_equal 123.456, first.single + assert_equal 123456.789, first.double + assert_equal(-::Float::INFINITY, second.single) + assert_equal ::Float::INFINITY, second.double + assert_same ::Float::NAN, third.double + end + + def test_update + record = PostgresqlNumber.create! single: "123.456", double: "123456.789" + new_single = 789.012 + new_double = 789012.345 + record.single = new_single + record.double = new_double + record.save! + + record.reload + assert_equal new_single, record.single + assert_equal new_double, record.double + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a71c0dfb26..a934180a43 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'support/ddl_helper' require 'support/connection_helper' @@ -54,6 +53,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do + assert_nil @connection.primary_key('ex') + end + end + def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') @@ -222,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 @@ -278,7 +283,7 @@ 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 @@ -292,9 +297,9 @@ 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 @@ -304,11 +309,8 @@ module ActiveRecord end def test_substitute_at - bind = @connection.substitute_at(nil, 0) - assert_equal Arel.sql('$1'), bind - - bind = @connection.substitute_at(nil, 1) - assert_equal Arel.sql('$2'), bind + bind = @connection.substitute_at(nil) + assert_equal Arel.sql('$1'), bind.to_sql end def test_partial_index @@ -434,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}" } @@ -454,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 11d5173d37..e4420d9d13 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,63 +10,33 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 't', @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 'f', @conn.type_cast(false, c) - end - - def test_type_cast_cidr - ip = IPAddr.new('255.0.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr') - assert_equal ip, @conn.type_cast(ip, c) - end - - def test_type_cast_inet - ip = IPAddr.new('255.1.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet') - assert_equal ip, @conn.type_cast(ip, c) + assert_equal 'f', @conn.type_cast(false) end def test_quote_float_nan nan = 0.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'NaN'", @conn.quote(nan, c) + assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity infinity = 1.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'Infinity'", @conn.quote(infinity, c) - end - - def test_quote_cast_numeric - fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') - assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') - assert_equal "'666'", @conn.quote(fixnum, c) - 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) + assert_equal "'Infinity'", @conn.quote(infinity) 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 d812cd01c4..b6b451ca5c 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -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 @@ -230,36 +230,14 @@ _SQL assert_nil_round_trip(@first_range, :int8_range, 39999...39999) end - def test_exclude_beginning_for_subtypes_with_succ_method_is_deprecated - tz = ::ActiveRecord::Base.default_timezone - - silence_warnings { - assert_deprecated { - range = PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") - assert_equal Date.new(2012, 1, 3)..Date.new(2012, 1, 4), range.date_range - } - assert_deprecated { - range = PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 1)..Time.send(tz, 2011, 1, 1, 14, 30, 0), range.ts_range - } - assert_deprecated { - range = PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") - assert_equal Time.parse('2010-01-01 09:30:01 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), range.tstz_range - } - assert_deprecated { - range = PostgresqlRange.create!(int4_range: "(1, 10]") - assert_equal 2..10, range.int4_range - } - assert_deprecated { - range = PostgresqlRange.create!(int8_range: "(10, 100]") - assert_equal 11..100, range.int8_range - } - } - end - def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } end def test_update_all_with_ranges 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..98291f1bbf --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -0,0 +1,89 @@ +require 'cases/helper' +require 'support/connection_helper' + +class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + include ConnectionHelper + + module MissingSuperuserPrivileges + def execute(sql) + if sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + super "BROKEN;" rescue nil # put transaction in broken state + raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege' + 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 + + 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 new file mode 100644 index 0000000000..f507328868 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -0,0 +1,34 @@ +require "cases/helper" + +class PostgresqlRenameTableTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :before_rename, force: true + end + + def teardown + @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 + # sanity check + assert_equal 1, num_indices_named("before_rename_pkey") + assert_equal 0, num_indices_named("after_rename_pkey") + + @connection.rename_table :before_rename, :after_rename + + assert_equal 0, num_indices_named("before_rename_pkey") + assert_equal 1, num_indices_named("after_rename_pkey") + end + + private + + def num_indices_named(name) + @connection.execute(<<-SQL).values.length + SELECT 1 FROM "pg_index" + JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" + WHERE "pg_class"."relname" = '#{name}' + SQL + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 99c26c4bf7..6937145439 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -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 9e5fd17dc4..77ff6d01bc 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -1,4 +1,6 @@ require "cases/helper" +require 'models/default' +require 'support/schema_dumping_helper' class SchemaTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -87,7 +89,7 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_names - assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names + assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names end def test_create_schema @@ -149,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 @@ -387,6 +389,14 @@ class SchemaTest < ActiveRecord::TestCase 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}')") + @connection.reset_pk_sequence! table_name + end + private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| @@ -403,7 +413,7 @@ class SchemaTest < ActiveRecord::TestCase def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do - indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name} + indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) assert_equal 4,indexes.size do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name) @@ -425,4 +435,82 @@ 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 + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + end + + def test_dump_foreign_key_targeting_different_schema + @connection.create_schema "my_schema" + @connection.create_table "my_schema.trains" do |t| + t.string :name + end + @connection.create_table "wagons" do |t| + t.integer :train_id + 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 + ensure + @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 + setup do + @connection = ActiveRecord::Base.connection + @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.execute "CREATE SCHEMA schema_1" + @connection.execute "CREATE DOMAIN schema_1.text AS text" + @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" + @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar" + + @old_search_path = @connection.schema_search_path + @connection.schema_search_path = "schema_1, pg_catalog" + @connection.create_table "defaults" do |t| + t.text "text_col", default: "some value" + t.string "string_col", default: "some value" + end + Default.reset_column_information + end + + teardown do + @connection.schema_search_path = @old_search_path + @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + Default.reset_column_information + end + + def test_text_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed" + end + + def test_string_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" + end + + def test_bpchar_defaults_in_new_schema_when_overriding_domain + @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" + Default.reset_column_information + assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed" + end + + def test_text_defaults_after_updating_column_default + @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" + assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db" + end + + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end end diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb deleted file mode 100644 index d7d40f6385..0000000000 --- a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require "cases/helper" - -class SqlTypesTest < ActiveRecord::TestCase - def test_binary_types - assert_equal 'bytea', type_to_sql(:binary, 100_000) - assert_raise ActiveRecord::ActiveRecordError do - type_to_sql :binary, 4294967295 - end - assert_equal 'text', type_to_sql(:text, 100_000) - assert_raise ActiveRecord::ActiveRecordError do - type_to_sql :text, 4294967295 - end - end - - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) - end -end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index eb32c4d2c2..da14063e20 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -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 23817198b1..c0907b8f21 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -12,4 +12,22 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase assert_equal ';', box_array.delimiter assert_equal ',', int_array.delimiter end + + test "array types correctly respect registration of subtypes" do + int_array = @connection.type_map.lookup(1007, -1, "integer[]") + bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") + big_array = [123456789123456789] + + 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 + int_range = @connection.type_map.lookup(3904, -1, "int4range") + bigint_range = @connection.type_map.lookup(3926, -1, "int8range") + big_range = 0..123456789123456789 + + 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/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index ae5b409f99..1219e197ab 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -1,8 +1,5 @@ -# encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' module PostgresqlUUIDHelper def connection @@ -10,12 +7,13 @@ 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 include PostgresqlUUIDHelper + include SchemaDumpingHelper class UUIDType < ActiveRecord::Base self.table_name = "uuid_data_type" @@ -50,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 + assert_not column.array? + + type = UUIDType.type_for_attribute("guid") + assert_not type.binary? end def test_treat_blank_uuid_as_nil @@ -70,13 +69,18 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal 'foobar', uuid.guid_before_type_cast end - def test_rfc_4122_regex + def test_acceptable_uuid_regex # Valid uuids ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', 'a0eebc999c0b4ef8bb6d6bb9bd380a11', 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', - '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid| + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}', + # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care, + # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here + # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + ].each do |valid_uuid| uuid = UUIDType.new guid: valid_uuid assert_not_nil uuid.guid end @@ -88,7 +92,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase 0.0, true, 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', - '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', 'a0eebc999r0b4ef8ab6d6bb9bd380a11', 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| @@ -108,10 +111,53 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid end end + + def test_schema_dump_with_shorthand + output = dump_table_schema "uuid_data_type" + assert_match %r{t\.uuid "guid"}, output + end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not duplicate.valid? + 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 include PostgresqlUUIDHelper + include SchemaDumpingHelper class UUID < ActiveRecord::Base self.table_name = 'pg_uuids' @@ -171,23 +217,22 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end def test_schema_dumper_for_uuid_primary_key - schema = StringIO.new - ActiveRecord::SchemaDumper.dump(connection, schema) - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) - assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema) + assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema) end def test_schema_dumper_for_uuid_primary_key_with_custom_default - schema = StringIO.new - ActiveRecord::SchemaDumper.dump(connection, schema) - assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string) - assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string) + schema = dump_table_schema "pg_uuids_2" + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema) end end end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper setup do enable_extension!('uuid-ossp', connection) @@ -211,6 +256,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first assert_nil col_desc["default"] end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end end end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 4165dd5ac9..b097deb2f4 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,7 +1,8 @@ -# encoding: utf-8 require 'cases/helper' +require 'support/schema_dumping_helper' class PostgresqlXMLTest < ActiveRecord::TestCase + include SchemaDumpingHelper class XmlDataType < ActiveRecord::Base self.table_name = 'xml_data_type' end @@ -21,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 @@ -45,4 +46,9 @@ class PostgresqlXMLTest < ActiveRecord::TestCase XmlDataType.update_all(payload: "<bar>baz</bar>") assert_equal "<bar>baz</bar>", data.reload.payload end + + def test_schema_dump_with_shorthand + output = dump_table_schema("xml_data_type") + assert_match %r{t\.xml "payload"}, output + end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index f1d6119d2e..7d66c44798 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -17,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ac8332e2fa..243f65df98 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,73 +15,52 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, column) + assert_equal expected, @conn.type_cast(binary) end def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo, nil) + assert_equal 'foo', @conn.type_cast(:foo) end def test_type_cast_date date = Date.today expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date, nil) + assert_equal expected, @conn.type_cast(date) end def test_type_cast_time time = Time.now expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time, nil) + assert_equal expected, @conn.type_cast(time) end def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10, nil) - assert_equal 2.2, @conn.type_cast(2.2, nil) + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) end def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil, nil) + assert_equal nil, @conn.type_cast(nil) end def test_type_cast_true - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) - end - - def test_type_cast_string - assert_equal '10', @conn.type_cast('10', nil) - - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 10, @conn.type_cast('10', c) - - c = Column.new(nil, 1, Type::Float.new) - assert_equal 10.1, @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Binary.new) - assert_equal '10.1', @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Date.new) - assert_equal '10.1', @conn.type_cast('10.1', c) + assert_equal 'f', @conn.type_cast(false) end def test_type_cast_bigdecimal bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd, nil) + assert_equal bd.to_f, @conn.type_cast(bd) end def test_type_cast_unknown_should_raise_error obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj, nil) } + assert_raise(TypeError) { @conn.type_cast(obj) } end def test_type_cast_object_which_responds_to_quoted_id @@ -94,21 +73,21 @@ module ActiveRecord 10 end }.new - assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + assert_equal 10, @conn.type_cast(quoted_id_obj) quoted_id_obj = Class.new { def quoted_id "'zomg'" end }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } end 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 8c1c22d3bf..5ca3c92027 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' @@ -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( @@ -133,8 +131,8 @@ module ActiveRecord end def test_bind_value_substitute - bind_param = @conn.substitute_at('foo', 0) - assert_equal Arel.sql('?'), bind_param + bind_param = @conn.substitute_at('foo') + assert_equal Arel.sql('?'), bind_param.to_sql end def test_exec_no_binds @@ -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 @@ -327,11 +324,11 @@ module ActiveRecord def test_columns with_example_table do - columns = @conn.columns('ex').sort_by { |x| x.name } + columns = @conn.columns('ex').sort_by(&:name) assert_equal 2, columns.length - assert_equal %w{ id number }.sort, columns.map { |x| x.name } - assert_equal [nil, nil], columns.map { |x| x.default } - assert_equal [true, true], columns.map { |x| x.null } + assert_equal %w{ id number }.sort, columns.map(&:name) + assert_equal [nil, nil], columns.map(&:default) + assert_equal [true, true], columns.map(&:null) end end @@ -405,6 +402,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do + assert_nil @conn.primary_key('ex') + end + end + def test_supports_extensions assert_not @conn.supports_extensions?, 'does not support extensions' end 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..deedf67c8e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'models/owner' diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index b6333240a8..f4e8003bc3 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -93,60 +93,38 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") end - def test_timestamps_without_null_is_deprecated_on_create_table - assert_deprecated do - ActiveRecord::Schema.define do - create_table :has_timestamps do |t| - t.timestamps - end + def test_timestamps_without_null_set_null_to_false_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps end end - end - def test_timestamps_without_null_is_deprecated_on_change_table - assert_deprecated do - ActiveRecord::Schema.define do - create_table :has_timestamps - - change_table :has_timestamps do |t| - t.timestamps - end - end - end + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null end - def test_no_deprecation_warning_from_timestamps_on_create_table - assert_not_deprecated do - ActiveRecord::Schema.define do - create_table :has_timestamps do |t| - t.timestamps null: true - end - - drop_table :has_timestamps + def test_timestamps_without_null_set_null_to_false_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps - create_table :has_timestamps do |t| - t.timestamps null: false - end + change_table :has_timestamps do |t| + t.timestamps default: Time.now end end - end - - def test_no_deprecation_warning_from_timestamps_on_change_table - assert_not_deprecated do - ActiveRecord::Schema.define do - create_table :has_timestamps - change_table :has_timestamps do |t| - t.timestamps null: true - end - drop_table :has_timestamps + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null + end - create_table :has_timestamps - change_table :has_timestamps do |t| - t.timestamps null: false, default: Time.now - end - end + def test_timestamps_without_null_set_null_to_false_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now end + + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null end end end 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 25555bd75c..47fd7345c8 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/topic' @@ -57,6 +58,85 @@ 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 + + comments = Class.new(ActiveRecord::Base) { + self.table_name = 'comments' + self.inheritance_column = 'not_there' + + posts = Class.new(ActiveRecord::Base) { + self.table_name = 'posts' + self.inheritance_column = 'not_there' + + default_scope -> { + counter += 1 + where("id = :inc", :inc => counter) + } + + has_many :comments, :class => comments + } + belongs_to :post, :class => posts, :inverse_of => false + } + + assert_equal 0, counter + comment = comments.first + assert_equal 0, counter + sql = capture_sql { comment.post } + comment.reload + assert_not_equal sql, capture_sql { comment.post } + end + def test_proxy_assignment account = Account.find(1) assert_nothing_raised { account.firm = account.firm } @@ -92,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 diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 5b7e462f64..a531e0e02c 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/author' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/company' class AssociationCallbacksTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb deleted file mode 100644 index 48f7ddbe83..0000000000 --- a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "cases/helper" - -class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase - class Post < ActiveRecord::Base - has_many :taggings, as: :taggable - has_many :tags, through: :taggings - end - - class Tagging < ActiveRecord::Base - belongs_to :taggable, polymorphic: true - belongs_to :tag - end - - class Tag < ActiveRecord::Base - end - - test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do - post = Post.create!(title: 'Hello', body: 'World!') - assert_deprecated { post.tags << Tag.create!(name: 'whatever') } - - assert_equal 1, post.tags.size - assert_equal 1, post.tags_count - assert_equal 1, post.reload.tags.size - assert_equal 1, post.reload.tags_count - end -end diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 0ff87d53ea..f571198079 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -70,9 +70,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase teardown do [Circle, Square, Triangle, PaintColor, PaintTexture, - ShapeExpression, NonPolyOne, NonPolyTwo].each do |c| - c.delete_all - end + ShapeExpression, NonPolyOne, NonPolyTwo].each(&:delete_all) end def generate_test_object_graphs diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index b852bd3536..7d8b933992 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -17,6 +17,7 @@ require 'models/subscriber' require 'models/subscription' require 'models/book' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/member' require 'models/membership' @@ -76,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 @@ -269,6 +278,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_three_level_nested_preloading_does_not_raise_exception_when_association_does_not_exist + post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id + + assert_nothing_raised do + Post.preload(:comments => [{:author => :essays}]).find(post_id) + end + end + def test_nested_loading_through_has_one_association aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length @@ -329,31 +346,31 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_limit comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a assert_equal 5, comments.length - assert_equal [1,2,3,5,6], comments.collect { |c| c.id } + assert_equal [1,2,3,5,6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect { |c| c.id } + assert_equal [5,6,7], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [3,5,6], comments.collect { |c| c.id } + assert_equal [3,5,6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect { |c| c.id } + assert_equal [6,7,8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect { |c| c.id } + assert_equal [6,7,8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name @@ -368,7 +385,7 @@ class EagerAssociationTest < ActiveRecord::TestCase comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a end assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect { |c| c.id } + assert_equal [5,6,7], comments.collect(&:id) assert_no_queries do comments.first.post end @@ -397,13 +414,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a assert_equal 1, posts.length - assert_equal [1], posts.collect { |p| p.id } + assert_equal [1], posts.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a assert_equal 1, posts.length - assert_equal [2], posts.collect { |p| p.id } + assert_equal [2], posts.collect(&:id) end def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name @@ -494,8 +511,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both - author = Author.all.merge!(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first - assert_equal [], author.special_nonexistant_post_comments + author = Author.all.merge!(:includes => :special_nonexistent_post_comments, :order => 'authors.id').first + assert_equal [], author.special_nonexistent_post_comments end def test_eager_with_has_many_through_join_model_with_conditions @@ -536,13 +553,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect { |p| p.id } + assert_equal [4,5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect { |p| p.id } + assert_equal [4,5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers @@ -817,18 +834,6 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end - def test_preload_with_interpolation - assert_deprecated do - post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions - end - - assert_deprecated do - post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions - end - end - def test_polymorphic_type_condition post = Post.all.merge!(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) @@ -903,6 +908,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries {assert_equal posts(:sti_comments), comment.post} end + def test_eager_association_with_scope_with_joins + assert_nothing_raised do + Post.includes(:very_special_comment_with_post_with_joins).to_a + end + end + def test_preconfigured_includes_with_has_many posts = authors(:david).posts_with_comments one = posts.detect { |p| p.id == 1 } @@ -935,6 +946,42 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end + def test_association_loading_notification + notifications = messages_for('instantiation.active_record') do + Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size + end + + message = notifications.first + payload = message.last + count = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size + + # eagerloaded row count should be greater than just developer count + assert_operator payload[:record_count], :>, count + assert_equal Developer.name, payload[:class_name] + end + + def test_base_messages + notifications = messages_for('instantiation.active_record') do + Developer.all.to_a + end + message = notifications.first + payload = message.last + + assert_equal Developer.all.to_a.count, payload[:record_count] + assert_equal Developer.name, payload[:class_name] + end + + def messages_for(name) + notifications = [] + ActiveSupport::Notifications.subscribe(name) do |*args| + notifications << args + end + yield + notifications + ensure + ActiveSupport::Notifications.unsubscribe(name) + end + def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass Comment.includes(:post).to_a @@ -1243,23 +1290,22 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet end - test "include instance dependent associations is deprecated" do + test "preloading and eager loading of instance dependent associations is not supported" do message = "association scope 'posts_with_signature' is" - assert_deprecated message do - begin - Author.includes(:posts_with_signature).to_a - rescue NoMethodError - # it's expected that preloading of this association fails - end + error = assert_raises(ArgumentError) do + Author.includes(:posts_with_signature).to_a end + assert_match message, error.message - assert_deprecated message do - Author.preload(:posts_with_signature).to_a rescue NoMethodError + error = assert_raises(ArgumentError) do + Author.preload(:posts_with_signature).to_a end + assert_match message, error.message - assert_deprecated message do + error = assert_raises(ArgumentError) do Author.eager_load(:posts_with_signature).to_a end + assert_match message, error.message end test "preloading readonly association" do @@ -1277,7 +1323,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end test "eager-loading readonly association" do - skip "eager_load does not yet preserve readonly associations" # has-one firm = Firm.where(id: "1").eager_load(:readonly_account).first! assert firm.readonly_account.readonly? @@ -1289,6 +1334,10 @@ class EagerAssociationTest < ActiveRecord::TestCase # has-many :through david = Author.where(id: "1").eager_load(:readonly_comments).first! assert david.readonly_comments.first.readonly? + + # belongs_to + post = Post.where(id: "1").eager_load(:author).first! + assert post.author.readonly? end test "preloading a polymorphic association with references to the associated table" do diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 4c1fdfdd9a..b161cde335 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/company_in_module' class AssociationsExtensionsTest < ActiveRecord::TestCase @@ -75,7 +76,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } - builder.define_extensions(model) + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 859310575e..aea9207bfe 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 @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/customer' @@ -78,9 +79,13 @@ class SubDeveloper < Developer :association_foreign_key => "developer_id" end +class DeveloperWithSymbolClassName < Developer + has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys +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 + :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers def setup_data_for_habtm_case ActiveRecord::Base.connection.execute('delete from countries_treaties') @@ -550,7 +555,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_dynamic_find_all_should_respect_readonly_access projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?} - projects(:active_record).readonly_developers.each { |d| d.readonly? } + projects(:active_record).readonly_developers.each(&:readonly?) end def test_new_with_values_in_collection @@ -883,4 +888,18 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase child.special_projects << SpecialProject.new("name" => "Special Project") assert child.save, 'child object should be saved' end + + def test_habtm_with_reflection_using_class_name_and_fixtures + assert_not_nil Developer._reflections['shared_computers'] + # Checking the fixture for named association is important here, because it's the only way + # we've been able to reproduce this bug + assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_equal developers(:david).shared_computers.first, computers(:laptop) + end + + def test_with_symbol_class_name + assert_nothing_raised NoMethodError do + DeveloperWithSymbolClassName.new + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e34b993029..897c52a49d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1,11 +1,13 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/contract' require 'models/topic' require 'models/reply' require 'models/category' +require 'models/image' require 'models/post' require 'models/author' require 'models/essay' @@ -28,7 +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 @@ -41,12 +48,59 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa end end +class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase + fixtures :authors, :essays, :subscribers, :subscriptions, :people + + def test_custom_primary_key_on_new_record_should_fetch_with_query + subscriber = Subscriber.new(nick: 'webster132') + assert !subscriber.subscriptions.loaded? + + assert_queries 1 do + assert_equal 2, subscriber.subscriptions.size + end + + assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132') + end + + def test_association_primary_key_on_new_record_should_fetch_with_query + author = Author.new(:name => "David") + assert !author.essays.loaded? + + assert_queries 1 do + assert_equal 1, author.essays.size + end + + assert_equal author.essays, Essay.where(writer_id: "David") + end + + def test_has_many_custom_primary_key + david = authors(:david) + assert_equal david.essays, Essay.where(writer_id: "David") + end + + def test_has_many_assignment_with_custom_primary_key + david = people(:david) + + assert_equal ["A Modest Proposal"], david.essays.map(&:name) + david.essays = [Essay.create!(name: "Remote Work" )] + assert_equal ["Remote Work"], david.essays.map(&:name) + end + + def test_blank_custom_primary_key_on_new_record_should_not_run_queries + author = Author.new + assert !author.essays.loaded? + + assert_queries 0 do + assert_equal 0, author.essays.size + end + end +end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations, :jobs, :tags + :posts, :readers, :taggings, :cars, :jobs, :tags, + :categorizations, :zines, :interests def setup Client.destroyed_client_ids.clear @@ -76,6 +130,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase dev.developer_projects.map(&:project_id).sort end + def test_default_scope_on_relations_is_not_cached + counter = 0 + posts = Class.new(ActiveRecord::Base) { + self.table_name = 'posts' + self.inheritance_column = 'not_there' + post = self + + comments = Class.new(ActiveRecord::Base) { + self.table_name = 'comments' + self.inheritance_column = 'not_there' + belongs_to :post, :class => post + default_scope -> { + counter += 1 + where("id = :inc", :inc => counter) + } + } + has_many :comments, :class => comments, :foreign_key => 'post_id' + } + assert_equal 0, counter + post = posts.first + assert_equal 0, counter + sql = capture_sql { post.comments.to_a } + post.comments.reset + assert_not_equal sql, capture_sql { post.comments.to_a } + end + def test_has_many_build_with_options college = College.create(name: 'UFMT') Student.create(active: true, college_id: college.id, name: 'Sarah') @@ -83,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 @@ -239,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 @@ -358,6 +462,32 @@ 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_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? } @@ -387,6 +517,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end + def test_update_all_on_association_accessed_before_save + firm = Firm.new(name: 'Firm') + firm.clients << Client.first + firm.save! + assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + end + def test_belongs_to_sanity c = Client.new assert_nil c.firm, "belongs_to failed sanity check on new object" @@ -555,17 +692,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_with_bang_on_has_many_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do firm = Firm.new firm.plain_clients.create! :name=>"Whoever" end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_regular_create_on_has_many_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do firm = Firm.new firm.plain_clients.create :name=>"Whoever" end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_create_with_bang_on_has_many_raises_when_record_not_saved @@ -576,9 +717,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_with_bang_on_habtm_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do Developer.new("name" => "Aredridel").projects.create! end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_adding_a_mismatch_class @@ -1179,7 +1322,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !clients.empty?, "37signals has clients after load" destroyed = companies(:first_firm).clients_of_firm.destroy_all assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) - assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" + assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" end @@ -1217,7 +1360,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 @@ -1319,10 +1461,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !account.valid? assert !orig_accounts.empty? - assert_raise ActiveRecord::RecordNotSaved do + error = assert_raise ActiveRecord::RecordNotSaved do firm.accounts = [account] end + assert_equal orig_accounts, firm.accounts + assert_equal "Failed to replace accounts because one or more of the " \ + "new records could not be saved.", error.message end def test_replace_with_same_content @@ -1534,39 +1679,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_custom_primary_key_on_new_record_should_fetch_with_query - author = Author.new(:name => "David") - assert !author.essays.loaded? - - assert_queries 1 do - assert_equal 1, author.essays.size - end - - assert_equal author.essays, Essay.where(writer_id: "David") - end - - def test_has_many_custom_primary_key - david = authors(:david) - assert_equal david.essays, Essay.where(writer_id: "David") - end - - def test_has_many_assignment_with_custom_primary_key - david = people(:david) - - assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] - assert_equal ["Remote Work"], david.essays.map(&:name) - end - - def test_blank_custom_primary_key_on_new_record_should_not_run_queries - author = Author.new - assert !author.essays.loaded? - - assert_queries 0 do - assert_equal 0, author.essays.size - end - end - def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') @@ -1619,6 +1731,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 @@ -1740,6 +1928,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [tagging], post.taggings end + def test_with_polymorphic_has_many_with_custom_columns_name + post = Post.create! :title => 'foo', :body => 'bar' + image = Image.create! + + post.images << image + + assert_equal [image], post.images + end + def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id welcome = posts(:welcome) tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange') @@ -1895,6 +2092,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id) end + test 'unscopes the default scope of associated model when used with include' do + car = Car.create! + bulb = Bulb.create! name: "other", car: car + + assert_equal bulb, Car.find(car.id).all_bulbs.first + assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first + end + test "raises RecordNotDestroyed when replaced child can't be destroyed" do car = Car.create! original_child = FailedBulb.create!(car: car) @@ -1955,4 +2160,68 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, car.bulbs.count assert_equal 1, car.tyres.count end + + test 'associations replace in memory when records have the same id' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + new_bulb.name = "foo" + car.bulbs = [new_bulb] + + assert_equal "foo", car.bulbs.first.name + end + + test 'in memory replacement executes no queries' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + + assert_no_queries do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements do not execute callbacks' do + raise_after_add = false + klass = Class.new(ActiveRecord::Base) do + self.table_name = :cars + has_many :bulbs, after_add: proc { raise if raise_after_add } + + def self.name + "Car" + end + end + bulb = Bulb.create! + car = klass.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + raise_after_add = true + + assert_nothing_raised do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements sets inverse instance' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + car.bulbs = [new_bulb] + + assert_same car, new_bulb.car + end + + test 'in memory replacement maintains order' do + first_bulb = Bulb.create! + second_bulb = Bulb.create! + car = Car.create!(bulbs: [first_bulb, second_bulb]) + + same_bulb = Bulb.find(first_bulb.id) + car.bulbs = [second_bulb, same_bulb] + + assert_equal [first_bulb, second_bulb], car.bulbs + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index cddf1a1f72..6729a5a9fc 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -15,6 +15,7 @@ require 'models/toy' require 'models/contract' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/subscriber' require 'models/book' require 'models/subscription' @@ -24,12 +25,13 @@ require 'models/categorization' require 'models/member' require 'models/membership' require 'models/club' +require 'models/organization' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, - :categories_posts, :clubs, :memberships + :categories_posts, :clubs, :memberships, :organizations # Dummies to force column loads so query counts are clean. def setup @@ -40,7 +42,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do - developers.each { |d| d.firms } + developers.each(&:firms) end end @@ -614,8 +616,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_create_on_new_record p = Post.new - assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") } - assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") } + error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") } + assert_equal "You cannot call create unless the parent is saved", error.message + + error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") } + assert_equal "You cannot call create unless the parent is saved", error.message end def test_associate_with_create_and_invalid_options @@ -1147,4 +1152,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club.members << member assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count end + + def test_build_for_has_many_through_association + organization = organizations(:nsa) + author = organization.author + post_direct = author.posts.build + post_through = organization.posts.build + assert_equal post_direct.author_id, post_through.author_id + 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 d412b3168e..3b6a4038cd 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/ship' @@ -7,6 +8,7 @@ require 'models/pirate' require 'models/car' require 'models/bulb' require 'models/author' +require 'models/image' require 'models/post' class HasOneAssociationsTest < ActiveRecord::TestCase @@ -235,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 @@ -271,6 +273,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.reload.account end + def test_create_with_inexistent_foreign_key_failing + firm = Firm.create(name: 'GlobalMegaCorp') + + assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) do + firm.create_account_with_inexistent_foreign_key + end + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -409,9 +419,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate = pirates(:redbeard) new_ship = Ship.new - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = new_ship end + + assert_equal "Failed to save the new associated ship.", error.message assert_nil pirate.ship assert_nil new_ship.pirate_id end @@ -421,20 +433,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate.ship.name = nil assert !pirate.ship.valid? - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = ships(:interceptor) end + assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id + assert_equal "Failed to remove the existing associated ship. " + + "The record failed to save after its foreign key was set to nil.", error.message end def test_replacement_failure_due_to_new_record_should_raise_error pirate = pirates(:blackbeard) new_ship = Ship.new - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = new_ship end + + assert_equal "Failed to save the new associated ship.", error.message assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id assert_equal pirate.id, ships(:black_pearl).reload.pirate_id @@ -557,6 +574,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal author.post, post end + def test_has_one_loading_for_new_record + post = Post.create!(author_id: 42, title: 'foo', body: 'bar') + author = Author.new(id: 42) + assert_equal post, author.post + end + def test_has_one_relationship_cannot_have_a_counter_cache assert_raise(ArgumentError) do Class.new(ActiveRecord::Base) do @@ -565,6 +588,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_with_polymorphic_has_one_with_custom_columns_name + post = Post.create! :title => 'foo', :body => 'bar' + image = Image.create! + + post.main_image = image + post.reload + + assert_equal image, post.main_image + end + test 'dangerous association name raises ArgumentError' do [:errors, 'errors', :save, 'save'].each do |name| assert_raises(ArgumentError, "Association #{name} should not be allowed") do diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 089cb0a3a2..f8772547a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -15,6 +15,7 @@ require 'models/essay' require 'models/owner' require 'models/post' require 'models/comment' +require 'models/categorization' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -289,6 +290,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_one_through_polymorphic_association + assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do + @member.premium_club + end + end + def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes minivan = minivans(:cool_first) diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 07cf65a760..b3fe759ad9 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -54,7 +54,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly authors = Author.joins(:posts) assert_not authors.empty?, "expected authors to be non-empty" - assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly" + assert authors.none?(&:readonly?), "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_honors_readonly_with_select @@ -102,7 +102,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_conditions_on_reflection assert !posts(:welcome).comments.empty? - assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!] + assert Post.joins(:nonexistent_comments).where(:id => posts(:welcome).id).empty? # [sic!] end def test_find_with_conditions_on_through_reflection diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 60df4e14dd..423b8238b1 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -10,6 +10,9 @@ require 'models/comment' require 'models/car' require 'models/bulb' require 'models/mixed_case_monkey' +require 'models/admin' +require 'models/admin/account' +require 'models/admin/user' class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -27,6 +30,15 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection" end + def test_has_many_and_belongs_to_should_find_inverse_automatically_for_model_in_module + account_reflection = Admin::Account.reflect_on_association(:users) + user_reflection = Admin::User.reflect_on_association(:account) + + assert_respond_to account_reflection, :has_inverse? + assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse" + assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection" + end + def test_has_one_and_belongs_to_should_find_inverse_automatically car_reflection = Car.reflect_on_association(:bulb) bulb_reflection = Bulb.reflect_on_association(:car) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index cace7ba142..9918601623 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -393,18 +393,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).taggings_2 + assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2 end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by(&:id) end def test_include_has_many_through_polymorphic_has_many author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) end end @@ -444,7 +444,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_uses_conditions_specified_on_the_has_many_association author = Author.first assert author.comments.present? - assert author.nonexistant_comments.blank? + assert author.nonexistent_comments.blank? end def test_has_many_through_uses_correct_attributes diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 321fb6c8dd..8b765a2e0c 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -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 9b0cf4c18f..3d202a5527 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/computer' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/categorization' @@ -42,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{ |x| x.name }.sort, firm.clients.collect{ |x| x.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") @@ -140,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 @@ -237,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 @@ -263,6 +242,11 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + test "first! works on loaded associations" do + david = authors(:david) + assert_equal david.posts.first, david.posts.reload.first! + end + def test_reset_unloads_target david = authors(:david) david.posts.reload 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 b4917e727a..243c90e945 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/minimalistic' require 'models/developer' +require 'models/computer' require 'models/auto_id' require 'models/boolean' require 'models/computer' @@ -501,7 +502,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_typecast_attribute_from_select_to_false Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT - if current_adapter?(:OracleAdapter) + if current_adapter?(:OracleAdapter, :FbAdapter) topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first else topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first @@ -512,7 +513,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_typecast_attribute_from_select_to_true Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT - if current_adapter?(:OracleAdapter) + if current_adapter?(:OracleAdapter, :FbAdapter) topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first else topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first @@ -530,20 +531,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_deprecated_cache_attributes - assert_deprecated do - Topic.cache_attributes :replies_count - end - - assert_deprecated do - Topic.cached_attributes - end - - assert_deprecated do - Topic.cache_attribute? :replies_count - end - end - def test_converted_values_are_returned_after_assignment developer = Developer.new(name: 1337, salary: "50000") @@ -670,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 @@ -681,6 +668,55 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_yaml_dumping_record_with_time_zone_aware_attribute + in_time_zone "Pacific Time (US & Canada)" do + record = Topic.new(id: 1) + record.written_on = "Jan 01 00:00:00 2014" + assert_equal record, YAML.load(YAML.dump(record)) + 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] @@ -722,17 +758,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_bulk_update_respects_access_control privatize("title=(value)") - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") } - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } + assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new(:title => "Rants about pants") } + assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end - def test_bulk_update_raise_unknown_attribute_errro - error = assert_raises(ActiveRecord::UnknownAttributeError) { - @target.new(:hello => "world") + def test_bulk_update_raise_unknown_attribute_error + error = assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) { + Topic.new(hello: "world") } - assert @target, error.record - assert "hello", error.attribute - assert "unknown attribute: hello", error.message + assert_instance_of Topic, error.record + assert_equal "hello", error.attribute + assert_equal "unknown attribute 'hello' for Topic.", error.message end def test_methods_override_in_multi_level_subclass @@ -893,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) @@ -905,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 dc20c3c676..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') @@ -109,7 +119,15 @@ module ActiveRecord test "fetch_value returns nil for unknown attributes" do attributes = attributes_with_uninitialized_key - assert_nil attributes.fetch_value(:wibble) + assert_nil attributes.fetch_value(:wibble) { "hello" } + end + + test "fetch_value returns nil for unknown attributes when types has a default" do + types = Hash.new(Type::Value.new) + builder = AttributeSet::Builder.new(types) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:wibble) { "hello" } end test "fetch_value uses the given block for uninitialized attributes" do @@ -123,13 +141,22 @@ module ActiveRecord assert_nil attributes.fetch_value(:bar) end + test "the primary_key is always initialized" do + builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo) + attributes = builder.build_from_database + + assert attributes.key?(:foo) + assert_equal [:foo], attributes.keys + assert attributes[:foo].initialized? + 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 @@ -161,5 +188,24 @@ module ActiveRecord builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) builder.build_from_database(foo: '1.1') end + + test "freezing doesn't prevent the set from materializing" do + builder = AttributeSet::Builder.new(foo: Type::String.new) + attributes = builder.build_from_database(foo: "1") + + 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 7b325abf1d..aa419c7a67 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -5,6 +5,7 @@ module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do @type = Minitest::Mock.new + @type.expect(:==, false, [false]) end teardown do @@ -12,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 @@ -21,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 @@ -30,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 @@ -41,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 @@ -57,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 @@ -89,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 @@ -101,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 @@ -168,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 79ef0502cb..e7b76b1cf9 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 @@ -20,7 +20,7 @@ end module ActiveRecord class CustomPropertiesTest < ActiveRecord::TestCase - def test_overloading_types + test "overloading types" do data = OverloadedType.new data.overloaded_float = "1.1" @@ -30,7 +30,7 @@ module ActiveRecord assert_equal 1.1, data.unoverloaded_float end - def test_overloaded_properties_save + test "overloaded properties save" do data = OverloadedType.new data.overloaded_float = "2.2" @@ -43,27 +43,27 @@ module ActiveRecord assert_kind_of Float, UnoverloadedType.last.overloaded_float end - def test_properties_assigned_in_constructor + test "properties assigned in constructor" do data = OverloadedType.new(overloaded_float: '3.3') assert_equal 3, data.overloaded_float end - def test_overloaded_properties_with_limit - assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit - assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + test "overloaded properties with limit" do + 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 - def test_nonexistent_attribute + test "nonexistent attribute" do data = OverloadedType.new(non_existent_decimal: 1) assert_equal BigDecimal.new(1), data.non_existent_decimal - assert_raise ActiveRecord::UnknownAttributeError do + assert_raise ActiveModel::AttributeAssignment::UnknownAttributeError do UnoverloadedType.new(non_existent_decimal: 1) end end - def test_changing_defaults + test "changing defaults" do data = OverloadedType.new unoverloaded_data = UnoverloadedType.new @@ -71,41 +71,90 @@ module ActiveRecord assert_equal 'the original default', unoverloaded_data.string_with_default end - def test_children_inherit_custom_properties + test "defaults are not touched on the columns" do + assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default + end + + test "children inherit custom properties" do data = ChildOfOverloadedType.new(overloaded_float: '4.4') assert_equal 4, data.overloaded_float end - def test_children_can_override_parents + test "children can override parents" do data = GrandchildOfOverloadedType.new(overloaded_float: '4.4') assert_equal 4.4, data.overloaded_float end - def test_overloading_properties_does_not_change_column_order - 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 - def test_caches_are_cleared + 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 + + 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 025cdbeba9..859afc4553 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,8 +1,10 @@ require 'cases/helper' require 'models/bird' +require 'models/comment' require 'models/company' require 'models/customer' require 'models/developer' +require 'models/computer' require 'models/invoice' require 'models/line_item' require 'models/order' @@ -19,6 +21,9 @@ require 'models/treasure' require 'models/eye' require 'models/electron' require 'models/molecule' +require 'models/member' +require 'models/member_detail' +require 'models/organization' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -613,6 +618,14 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase firm.save! assert !account.persisted? end + + def test_autosave_new_record_with_after_create_callback + post = PostWithAfterCreateCallback.new(title: 'Captain Murphy', body: 'is back') + post.comments.build(body: 'foo') + post.save! + + assert_not_nil post.author_id + end end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @@ -761,13 +774,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } - assert !@pirate.birds.any? { |child| child.marked_for_destruction? } + assert !@pirate.birds.any?(&:marked_for_destruction?) - @pirate.birds.each { |child| child.mark_for_destruction } + @pirate.birds.each(&:mark_for_destruction) klass = @pirate.birds.first.class ids = @pirate.birds.map(&:id) - assert @pirate.birds.all? { |child| child.marked_for_destruction? } + assert @pirate.birds.all?(&:marked_for_destruction?) ids.each { |id| assert klass.find_by_id(id) } @pirate.save @@ -801,14 +814,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.each { |bird| bird.name = '' } assert !@pirate.valid? - @pirate.birds.each { |bird| bird.destroy } + @pirate.birds.each(&:destroy) assert @pirate.valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many @pirate.birds.create!(:name => "birds_1") - @pirate.birds.each { |bird| bird.mark_for_destruction } + @pirate.birds.each(&:mark_for_destruction) assert @pirate.save @pirate.birds.each { |bird| bird.expects(:destroy).never } @@ -875,7 +888,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase association_name_with_callbacks = "birds_with_#{callback_type}_callbacks" @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") - @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction) child_id = @pirate.send(association_name_with_callbacks).first.id @pirate.ship_log.clear @@ -893,8 +906,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } - assert !@pirate.parrots.any? { |parrot| parrot.marked_for_destruction? } - @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + assert !@pirate.parrots.any?(&:marked_for_destruction?) + @pirate.parrots.each(&:mark_for_destruction) assert_no_difference "Parrot.count" do @pirate.save @@ -927,14 +940,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.parrots.each { |parrot| parrot.name = '' } assert !@pirate.valid? - @pirate.parrots.each { |parrot| parrot.destroy } + @pirate.parrots.each(&:destroy) assert @pirate.valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm @pirate.parrots.create!(:name => "parrots_1") - @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + @pirate.parrots.each(&:mark_for_destruction) assert @pirate.save Pirate.transaction do @@ -979,7 +992,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks" @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") - @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction) child_id = @pirate.send(association_name_with_callbacks).first.id @pirate.ship_log.clear @@ -1017,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! @@ -1038,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 @@ -1116,6 +1144,27 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end end +class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase + self.use_transactional_fixtures = false unless supports_savepoints? + + def setup + super + organization = Organization.create + @member = Member.create + MemberDetail.create(organization: organization, member: @member) + end + + def test_should_not_has_one_through_model + class << @member.organization + def save(*args) + super + raise 'Oh noes!' + end + end + assert_nothing_raised { @member.save } + end +end + class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index fb535e74fc..993350ebd6 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "cases/helper" require 'active_support/concurrency/latch' @@ -10,6 +9,7 @@ require 'models/category' require 'models/company' require 'models/customer' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/default' require 'models/auto_id' @@ -87,6 +87,7 @@ class BasicsTest < ActiveRecord::TestCase 'Mysql2Adapter' => '`', 'PostgreSQLAdapter' => '"', 'OracleAdapter' => '"', + 'FbAdapter' => '"' }.fetch(classname) { raise "need a bad char for #{classname}" } @@ -110,7 +111,7 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end - unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) + unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) def test_limit_with_comma assert Topic.limit("1,2").to_a end @@ -522,6 +523,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) end + def test_find_by_slug_with_range + assert_equal Topic.where(id: '1-meowmeow'..'2-hello'), Topic.where(id: 1..2) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new assert_equal false, Topic.new == Topic.new @@ -807,7 +812,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 @@ -893,101 +897,13 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 'a text field', default.char3 end end - - class Geometric < ActiveRecord::Base; end - def test_geometric_content - - # accepted format notes: - # ()'s aren't required - # values can be a mix of float or integer - - g = Geometric.new( - :a_point => '(5.0, 6.1)', - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '(2.0, 3), (5.5, 7.0)', - :a_box => '2.0, 3, 5.5, 7.0', - :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', # [ ] is an open path - :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', - :a_circle => '<(5.3, 10.4), 2>' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - - # use a geometric function to test for an open path - objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id] - - assert_equal true, objs[0].isopen - - # test alternate formats when defining the geometric types - - g = Geometric.new( - :a_point => '5.0, 6.1', - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '((2.0, 3), (5.5, 7.0))', - :a_box => '(2.0, 3), (5.5, 7.0)', - :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path - :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', - :a_circle => '((5.3, 10.4), 2)' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - - # use a geometric function to test for an closed path - objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id] - - assert_equal true, objs[0].isclosed - - # test native ruby formats when defining the geometric types - g = Geometric.new( - :a_point => [5.0, 6.1], - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '((2.0, 3), (5.5, 7.0))', - :a_box => '(2.0, 3), (5.5, 7.0)', - :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path - :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', - :a_circle => '((5.3, 10.4), 2)' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - end end 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 :my_house_population, :integer + attribute :atoms_in_universe, :integer end def test_big_decimal_conditions @@ -1383,7 +1299,10 @@ class BasicsTest < ActiveRecord::TestCase c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! c2 = Post.connection.schema_cache.columns('posts') - assert_not_equal c1, c2 + c1.each_with_index do |v, i| + assert_not_same v, c2[i] + end + assert_equal c1, c2 end def test_current_scope_is_reset @@ -1507,7 +1426,7 @@ class BasicsTest < ActiveRecord::TestCase attrs.delete 'id' typecast = Class.new(ActiveRecord::Type::Value) { - def type_cast value + def cast value "t.lo" end } @@ -1607,4 +1526,14 @@ class BasicsTest < ActiveRecord::TestCase test "records without an id have unique hashes" do assert_not_equal Post.new.hash, Post.new.hash end + + test "resetting column information doesn't remove attribute methods" do + topic = topics(:first) + + assert_not topic.id_changed? + + Topic.reset_column_information + + assert_not topic.id_changed? + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c12fa03015..9e428098e4 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 @@ -200,11 +209,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 0bc7ee6d64..1e38b97c4a 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -1,9 +1,11 @@ require 'cases/helper' require 'models/topic' +require 'models/author' +require 'models/post' module ActiveRecord class BindParameterTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :authors, :posts class LogListener attr_accessor :calls @@ -20,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 @@ -30,40 +32,34 @@ module ActiveRecord end if ActiveRecord::Base.connection.supports_statement_cache? - def test_binds_are_logged - sub = @connection.substitute_at(@pk, 0) - binds = [[@pk, 1]] - sql = "select * from topics where id = #{sub}" - - @connection.exec_query(sql, 'SQL', binds) - - message = @subscriber.calls.find { |args| args[4][:sql] == sql } - assert_equal binds, message[4][:binds] + def test_bind_from_join_in_subquery + subquery = Author.joins(:thinking_posts).where(name: 'David') + scope = Author.from(subquery, 'authors').where(id: 1) + assert_equal 1, scope.count end - def test_binds_are_logged_after_type_cast - sub = @connection.substitute_at(@pk, 0) - binds = [[@pk, "3"]] - sql = "select * from topics where id = #{sub}" + def test_binds_are_logged + sub = @connection.substitute_at(@pk) + binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] + 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] + assert_equal binds, 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 ec6a319ab5..f0393aa6b1 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -10,13 +10,14 @@ require 'models/reply' require 'models/minivan' require 'models/speedometer' require 'models/ship_part' +require 'models/treasure' 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 @@ -461,7 +462,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 7, Company.includes(:contracts).sum(:developer_id) end - def test_from_option_with_specified_index if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) @@ -610,4 +610,30 @@ class CalculationsTest < ActiveRecord::TestCase .pluck('topics.title', 'replies_topics.title') assert_equal expected, actual end + + def test_calculation_with_polymorphic_relation + part = ShipPart.create!(name: "has trinket") + part.trinkets.create! + + assert_equal part.id, ShipPart.joins(:trinkets).sum(:id) + end + + def test_pluck_joined_with_polymorphic_relation + part = ShipPart.create!(name: "has trinket") + part.trinkets.create! + + assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id) + 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 end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index c8f56e3c73..3ae4a6eade 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -1,4 +1,6 @@ require "cases/helper" +require 'models/developer' +require 'models/computer' class CallbackDeveloper < ActiveRecord::Base self.table_name = 'developers' @@ -47,6 +49,11 @@ class CallbackDeveloperWithFalseValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } end +class CallbackDeveloperWithHaltedValidation < CallbackDeveloper + before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) } + before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } +end + class ParentDeveloper < ActiveRecord::Base self.table_name = 'developers' attr_accessor :after_save_called @@ -57,27 +64,6 @@ class ChildDeveloper < ParentDeveloper end -class RecursiveCallbackDeveloper < ActiveRecord::Base - self.table_name = 'developers' - - before_save :on_before_save - after_save :on_after_save - - attr_reader :on_before_save_called, :on_after_save_called - - def on_before_save - @on_before_save_called ||= 0 - @on_before_save_called += 1 - save unless @on_before_save_called > 1 - end - - def on_after_save - @on_after_save_called ||= 0 - @on_after_save_called += 1 - save unless @on_after_save_called > 1 - end -end - class ImmutableDeveloper < ActiveRecord::Base self.table_name = 'developers' @@ -86,35 +72,24 @@ class ImmutableDeveloper < ActiveRecord::Base before_save :cancel before_destroy :cancel - def cancelled? - @cancelled == true - end - private def cancel - @cancelled = true false end end -class ImmutableMethodDeveloper < ActiveRecord::Base +class DeveloperWithCanceledCallbacks < ActiveRecord::Base self.table_name = 'developers' - validates_inclusion_of :salary, :in => 50000..200000 - - def cancelled? - @cancelled == true - end + validates_inclusion_of :salary, in: 50000..200000 - before_save do - @cancelled = true - false - end + before_save :cancel + before_destroy :cancel - before_destroy do - @cancelled = true - false - end + private + def cancel + throw(:abort) + end end class OnCallbacksDeveloper < ActiveRecord::Base @@ -180,6 +155,23 @@ class CallbackCancellationDeveloper < ActiveRecord::Base after_destroy { @after_destroy_called = true } end +class CallbackHaltedDeveloper < ActiveRecord::Base + self.table_name = 'developers' + + attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called + attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy + + before_save { throw(:abort) if defined?(@cancel_before_save) } + before_create { throw(:abort) if @cancel_before_create } + before_update { throw(:abort) if @cancel_before_update } + before_destroy { throw(:abort) if @cancel_before_destroy } + + after_save { @after_save_called = true } + after_update { @after_update_called = true } + after_create { @after_create_called = true } + after_destroy { @after_destroy_called = true } +end + class CallbacksTest < ActiveRecord::TestCase fixtures :developers @@ -296,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 @@ -365,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 @@ -416,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 @@ -437,11 +444,14 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end - def test_before_save_returning_false + def test_deprecated_before_save_returning_false david = ImmutableDeveloper.find(1) - assert david.valid? - assert !david.save - assert_raise(ActiveRecord::RecordNotSaved) { david.save! } + assert_deprecated do + assert david.valid? + assert !david.save + exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } + assert_equal exc.record, david + end david = ImmutableDeveloper.find(1) david.salary = 10_000_000 @@ -451,37 +461,48 @@ class CallbacksTest < ActiveRecord::TestCase someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_save = true - assert someone.valid? - assert !someone.save + assert_deprecated do + assert someone.valid? + assert !someone.save + end assert_save_callbacks_not_called(someone) end - def test_before_create_returning_false + def test_deprecated_before_create_returning_false someone = CallbackCancellationDeveloper.new someone.cancel_before_create = true - assert someone.valid? - assert !someone.save + assert_deprecated do + assert someone.valid? + assert !someone.save + end assert_save_callbacks_not_called(someone) end - def test_before_update_returning_false + def test_deprecated_before_update_returning_false someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_update = true - assert someone.valid? - assert !someone.save + assert_deprecated do + assert someone.valid? + assert !someone.save + end assert_save_callbacks_not_called(someone) end - def test_before_destroy_returning_false + def test_deprecated_before_destroy_returning_false david = ImmutableDeveloper.find(1) - assert !david.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } + assert_deprecated do + assert !david.destroy + exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } + assert_equal exc.record, david + end assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_destroy = true - assert !someone.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } + assert_deprecated do + assert !someone.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } + end assert !someone.after_destroy_called end @@ -492,9 +513,59 @@ class CallbacksTest < ActiveRecord::TestCase end private :assert_save_callbacks_not_called + def test_before_create_throwing_abort + someone = CallbackHaltedDeveloper.new + someone.cancel_before_create = true + assert someone.valid? + assert !someone.save + assert_save_callbacks_not_called(someone) + end + + def test_before_save_throwing_abort + david = DeveloperWithCanceledCallbacks.find(1) + assert david.valid? + assert !david.save + exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } + assert_equal exc.record, david + + david = DeveloperWithCanceledCallbacks.find(1) + david.salary = 10_000_000 + assert !david.valid? + assert !david.save + assert_raise(ActiveRecord::RecordInvalid) { david.save! } + + someone = CallbackHaltedDeveloper.find(1) + someone.cancel_before_save = true + assert someone.valid? + assert !someone.save + assert_save_callbacks_not_called(someone) + end + + def test_before_update_throwing_abort + someone = CallbackHaltedDeveloper.find(1) + someone.cancel_before_update = true + assert someone.valid? + assert !someone.save + assert_save_callbacks_not_called(someone) + end + + def test_before_destroy_throwing_abort + david = DeveloperWithCanceledCallbacks.find(1) + assert !david.destroy + exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } + assert_equal exc.record, david + assert_not_nil ImmutableDeveloper.find_by_id(1) + + someone = CallbackHaltedDeveloper.find(1) + someone.cancel_before_destroy = true + assert !someone.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } + assert !someone.after_destroy_called + end + def test_callback_returning_false david = CallbackDeveloperWithFalseValidation.find(1) - david.save + assert_deprecated { david.save } assert_equal [ [ :after_find, :method ], [ :after_find, :string ], @@ -520,6 +591,34 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end + def test_callback_throwing_abort + david = CallbackDeveloperWithHaltedValidation.find(1) + david.save + assert_equal [ + [ :after_find, :method ], + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :method ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :method ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation, :throwing_abort ], + [ :after_rollback, :block ], + [ :after_rollback, :object ], + [ :after_rollback, :proc ], + [ :after_rollback, :string ], + [ :after_rollback, :method ], + ], david.history + end + def test_inheritance_of_callbacks parent = ParentDeveloper.new assert !parent.after_save_called 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/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 3e33b30144..b72f8ca88c 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -44,9 +44,7 @@ module ActiveRecord end def test_connection_pools - assert_deprecated do - assert_equal({ Base.connection_pool.spec => @pool }, @handler.connection_pools) - end + assert_equal([@pool], @handler.connection_pools) end end end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index e1b2804a18..9ee92a3cd2 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -5,10 +5,14 @@ module ActiveRecord class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase def setup @previous_database_url = ENV.delete("DATABASE_URL") + @previous_rack_env = ENV.delete("RACK_ENV") + @previous_rails_env = ENV.delete("RAILS_ENV") end teardown do ENV["DATABASE_URL"] = @previous_database_url + ENV["RACK_ENV"] = @previous_rack_env + ENV["RAILS_ENV"] = @previous_rails_env end def resolve_config(config) @@ -27,11 +31,23 @@ module ActiveRecord assert_equal expected, actual end - def test_resolver_with_database_uri_and_and_current_env_string_key + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rails_env ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = assert_deprecated { resolve_spec("default_env", config) } - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + ENV['RAILS_ENV'] = "foo" + + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env + ENV['DATABASE_URL'] = "postgres://localhost/foo" + ENV['RACK_ENV'] = "foo" + + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } assert_equal expected, actual end @@ -51,16 +67,6 @@ module ActiveRecord end end - def test_resolver_with_database_uri_and_unknown_string_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - assert_deprecated do - assert_raises AdapterNotSpecified do - resolve_spec("production", config) - end - end - end - def test_resolver_with_database_uri_and_supplied_url ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } @@ -139,6 +145,51 @@ module ActiveRecord assert_equal nil, actual["production"] assert_equal nil, actual["development"] assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_blank_with_database_url_with_rails_env + ENV['RAILS_ENV'] = "not_production" + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + + assert_equal expected, actual["not_production"] + assert_equal nil, actual["production"] + assert_equal nil, actual["default_env"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:not_production] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_blank_with_database_url_with_rack_env + ENV['RACK_ENV'] = "not_production" + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + + assert_equal expected, actual["not_production"] + assert_equal nil, actual["production"] + assert_equal nil, actual["default_env"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:not_production] assert_equal nil, actual[:production] assert_equal nil, actual[:development] assert_equal nil, actual[:test] diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index d4d67487db..80244d1439 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -22,6 +22,10 @@ module ActiveRecord assert_lookup_type :string, "SET('one', 'two', 'three')" end + def test_enum_type_with_value_matching_other_type + assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" + end + def test_binary_types assert_lookup_type :binary, 'bit' assert_lookup_type :binary, 'BIT' diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index d5c1dc1e5d..05c57985a1 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -79,13 +79,18 @@ module ActiveRecord assert_lookup_type :integer, 'bigint' end + def test_bigint_limit + cast_type = @connection.type_map.lookup("bigint") + assert_equal 8, cast_type.limit + 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 77d9ae9b8e..f53c496ecd 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -96,6 +96,14 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end + def test_connections_closed_if_exception_and_explicitly_not_test + @env['rack.test'] = false + app = Class.new(App) { def call(env); raise NotImplementedError; end }.new + explosive = ConnectionManagement.new(app) + assert_raises(NotImplementedError) { explosive.call(@env) } + assert !ActiveRecord::Base.connection_handler.active_connections? + end + test "doesn't clear active connections when running in a test case" do @env['rack.test'] = true @management.call(@env) 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..6a4e64b22c --- /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_fixtures = 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 c0491bbee5..4cbff564aa 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -3,6 +3,8 @@ require 'models/topic' require 'models/task' class DateTimeTest < ActiveRecord::TestCase + include InTimeZone + def test_saves_both_date_and_time with_env_tz 'America/New_York' do with_timezone_config default: :utc do @@ -29,6 +31,14 @@ class DateTimeTest < ActiveRecord::TestCase assert_nil task.ending end + def test_assign_bad_date_time_with_timezone + in_time_zone "Pacific Time (US & Canada)" do + task = Task.new + task.starting = '2014-07-01T24:59:59GMT' + assert_nil task.starting + end + end + def test_assign_empty_date topic = Topic.new topic.last_read = '' @@ -40,4 +50,12 @@ class DateTimeTest < ActiveRecord::TestCase topic.bonus_time = '' assert_nil topic.bonus_time end + + def test_assign_in_local_timezone + now = DateTime.now + with_timezone_config default: :local do + task = Task.new starting: now + 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 c089e63128..b9db0d0123 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -18,25 +18,48 @@ class DefaultTest < ActiveRecord::TestCase end end - if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - def test_default_integers - default = Default.new - assert_instance_of Fixnum, default.positive_integer - assert_equal 1, default.positive_integer - assert_instance_of Fixnum, default.negative_integer - assert_equal(-1, default.negative_integer) - assert_instance_of BigDecimal, default.decimal_number - assert_equal BigDecimal.new("2.78"), default.decimal_number - end - end - if current_adapter?(:PostgreSQLAdapter) def test_multiline_default_text + record = Default.new # older postgres versions represent the default with escapes ("\\012" for a newline) - assert( "--- []\n\n" == Default.columns_hash['multiline_default'].default || - "--- []\\012\\012" == Default.columns_hash['multiline_default'].default) + assert("--- []\n\n" == record.multiline_default || "--- []\\012\\012" == record.multiline_default) + end + end +end + +class DefaultNumbersTest < ActiveRecord::TestCase + class DefaultNumber < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :default_numbers do |t| + t.integer :positive_integer, default: 7 + t.integer :negative_integer, default: -5 + t.decimal :decimal_number, default: "2.78", precision: 5, scale: 2 end end + + teardown do + @connection.drop_table :default_numbers, if_exists: true + end + + def test_default_positive_integer + record = DefaultNumber.new + assert_equal 7, record.positive_integer + assert_equal "7", record.positive_integer_before_type_cast + end + + def test_default_negative_integer + record = DefaultNumber.new + assert_equal (-5), record.negative_integer + assert_equal "-5", record.negative_integer_before_type_cast + end + + def test_default_decimal_number + record = DefaultNumber.new + assert_equal BigDecimal.new("2.78"), record.decimal_number + assert_equal "2.78", record.decimal_number_before_type_cast + end end class DefaultStringsTest < ActiveRecord::TestCase @@ -99,19 +122,21 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_text_not_null_defaults_non_strict using_strict(false) do with_text_blob_not_null_table do |klass| - assert_equal '', klass.columns_hash['non_null_blob'].default - assert_equal '', klass.columns_hash['non_null_text'].default + record = klass.new + assert_equal '', record.non_null_blob + assert_equal '', record.non_null_text - assert_nil klass.columns_hash['null_blob'].default - assert_nil klass.columns_hash['null_text'].default + assert_nil record.null_blob + assert_nil record.null_text - instance = klass.create! + record.save! + record.reload - assert_equal '', instance.non_null_text - assert_equal '', instance.non_null_blob + assert_equal '', record.non_null_text + assert_equal '', record.non_null_blob - assert_nil instance.null_text - assert_nil instance.null_blob + assert_nil record.null_text + assert_nil record.null_blob end end end @@ -119,10 +144,11 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_text_not_null_defaults_strict using_strict(true) do with_text_blob_not_null_table do |klass| - assert_nil klass.columns_hash['non_null_blob'].default - assert_nil klass.columns_hash['non_null_text'].default - assert_nil klass.columns_hash['null_blob'].default - assert_nil klass.columns_hash['null_text'].default + record = klass.new + assert_nil record.non_null_blob + assert_nil record.non_null_text + assert_nil record.null_blob + assert_nil record.null_text assert_raises(ActiveRecord::StatementInvalid) { klass.create } end @@ -172,48 +198,3 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end end - -if current_adapter?(:PostgreSQLAdapter) - class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase - def setup - @connection = ActiveRecord::Base.connection - - @old_search_path = @connection.schema_search_path - @connection.schema_search_path = "schema_1, pg_catalog" - @connection.create_table "defaults" do |t| - t.text "text_col", :default => "some value" - t.string "string_col", :default => "some value" - end - Default.reset_column_information - end - - def test_text_defaults_in_new_schema_when_overriding_domain - assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse" - end - - def test_string_defaults_in_new_schema_when_overriding_domain - assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse" - end - - def test_bpchar_defaults_in_new_schema_when_overriding_domain - @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" - Default.reset_column_information - assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse" - end - - def test_text_defaults_after_updating_column_default - @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" - assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" - end - - def test_default_containing_quote_and_colons - @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" - assert_equal "foo'::bar", Default.new.string_col - end - - teardown do - @connection.schema_search_path = @old_search_path - Default.reset_column_information - end - end -end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index eb9b1a2d74..3a7cc572e6 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -165,18 +165,6 @@ class DirtyTest < ActiveRecord::TestCase assert_equal parrot.name_change, parrot.title_change end - def test_reset_attribute! - pirate = Pirate.create!(:catchphrase => 'Yar!') - pirate.catchphrase = 'Ahoy!' - - assert_deprecated do - pirate.reset_catchphrase! - end - assert_equal "Yar!", pirate.catchphrase - assert_equal Hash.new, pirate.changes - assert !pirate.catchphrase_changed? - end - def test_restore_attribute! pirate = Pirate.create!(:catchphrase => 'Yar!') pirate.catchphrase = 'Ahoy!' @@ -635,13 +623,13 @@ class DirtyTest < ActiveRecord::TestCase end end - test "defaults with type that implements `type_cast_for_database`" do + test "defaults with type that implements `serialize`" do type = Class.new(ActiveRecord::Type::Value) do - def type_cast(value) + def cast(value) value.to_i end - def type_cast_for_database(value) + def serialize(value) value.to_s end end @@ -688,7 +676,14 @@ class DirtyTest < ActiveRecord::TestCase serialize :data end - klass.create!(data: "foo") + binary = klass.create!(data: "\\\\foo") + + assert_not binary.changed? + + binary.data = binary.data.dup + + assert_not binary.changed? + binary = klass.last assert_not binary.changed? @@ -698,6 +693,42 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do + test_type_class = Class.new(ActiveRecord::Type::Value) do + define_method(:changed_in_place?) do |*| + raise + end + end + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + attribute :foo, test_type_class.new + end + + model = klass.new(first_name: "Jim") + 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 + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 346fcab6ea..3b7bbcf47a 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -26,6 +26,49 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.unread.first end + test "find via where with values" do + proposed, written = Book.statuses[:proposed], Book.statuses[:written] + + assert_equal @book, Book.where(status: proposed).first + refute_equal @book, Book.where(status: written).first + assert_equal @book, Book.where(status: [proposed]).first + refute_equal @book, Book.where(status: [written]).first + refute_equal @book, Book.where("status <> ?", proposed).first + assert_equal @book, Book.where("status <> ?", written).first + end + + test "find via where with symbols" do + assert_equal @book, Book.where(status: :proposed).first + refute_equal @book, Book.where(status: :written).first + assert_equal @book, Book.where(status: [:proposed]).first + refute_equal @book, Book.where(status: [:written]).first + refute_equal @book, Book.where.not(status: :proposed).first + assert_equal @book, Book.where.not(status: :written).first + end + + test "find via where with strings" do + assert_equal @book, Book.where(status: "proposed").first + refute_equal @book, Book.where(status: "written").first + assert_equal @book, Book.where(status: ["proposed"]).first + refute_equal @book, Book.where(status: ["written"]).first + refute_equal @book, Book.where.not(status: "proposed").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? @@ -129,19 +172,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 @@ -161,7 +209,11 @@ class EnumTest < ActiveRecord::TestCase 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 "proposed", @book.status_before_type_cast + else + assert_equal "proposed", @book.status + end end test "reserved enum names" do @@ -177,9 +229,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 +252,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 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 fc91b728c2..39308866ee 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -10,13 +10,16 @@ require 'models/reply' require 'models/entrant' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/customer' require 'models/toy' require 'models/matey' require 'models/dog' +require 'models/car' +require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -52,10 +55,13 @@ class FinderTest < ActiveRecord::TestCase end def test_symbols_table_ref + gc_disabled = GC.disable Post.where("author_id" => nil) # warm up x = Symbol.all_symbols.count Post.where("title" => {"xxxqqqq" => "bar"}) assert_equal x, Symbol.all_symbols.count + ensure + GC.enable if gc_disabled == false end # find should handle strings that come from URLs @@ -99,21 +105,10 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_fails_when_parameter_has_invalid_type - if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter) - assert_raises ActiveRecord::StatementInvalid do - Topic.exists?(("9"*53).to_i) # number that's bigger than int - end - else + assert_raises(RangeError) do assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end - - if current_adapter?(:PostgreSQLAdapter) - assert_raises ActiveRecord::StatementInvalid do - Topic.exists?("foo") - end - else - assert_equal false, Topic.exists?("foo") - end + assert_equal false, Topic.exists?("foo") end def test_exists_does_not_select_columns_without_alias @@ -209,6 +204,28 @@ class FinderTest < ActiveRecord::TestCase assert_equal 2, last_devs.size end + def test_find_with_large_number + assert_raises(ActiveRecord::RecordNotFound) { Topic.find('9999999999999999999999999999999') } + end + + def test_find_by_with_large_number + assert_nil Topic.find_by(id: '9999999999999999999999999999999') + end + + def test_find_by_id_with_large_number + assert_nil Topic.find_by_id('9999999999999999999999999999999') + end + + def test_find_on_relation_with_large_number + assert_nil Topic.where('1=1').find_by(id: 9999999999999999999999999999999) + end + + def test_find_by_bang_on_relation_with_large_number + assert_raises(ActiveRecord::RecordNotFound) do + Topic.where('1=1').find_by!(id: 9999999999999999999999999999999) + end + end + def test_find_an_empty_array assert_equal [], Topic.find([]) end @@ -262,7 +279,7 @@ class FinderTest < ActiveRecord::TestCase end def test_take_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").take! end end @@ -282,7 +299,7 @@ class FinderTest < ActiveRecord::TestCase end def test_first_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").first! end end @@ -296,7 +313,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.first! end end @@ -318,7 +335,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_second_bang assert Topic.second! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.second! end end @@ -340,7 +357,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_third_bang assert Topic.third! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.third! end end @@ -362,7 +379,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_fourth_bang assert Topic.fourth! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.fourth! end end @@ -384,7 +401,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_fifth_bang assert Topic.fifth! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.fifth! end end @@ -396,14 +413,14 @@ class FinderTest < ActiveRecord::TestCase end def test_last_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").last! end end def test_model_class_responds_to_last_bang assert_equal topics(:fifth), Topic.last! - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.delete_all Topic.last! end @@ -477,6 +494,12 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } end + def test_find_on_combined_explicit_and_hashed_table_names + assert Topic.where('topics.approved' => false, topics: { author_name: "David" }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true, topics: { author_name: "David" }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => false, topics: { author_name: "Melanie" }).find(1) } + end + def test_find_with_hash_conditions_on_joined_table firms = Firm.joins(:account).where(:accounts => { :credit_limit => 50 }) assert_equal 1, firms.size @@ -525,30 +548,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort end - def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges - assert_deprecated do - assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [[1..2], 3, [5], 6..8, 9]).to_a.map(&:id).sort - end - end - - def test_find_on_hash_conditions_with_array_of_integers_and_arrays - assert_deprecated do - assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [[1, 2], 3, 5, [6, [7], 8], 9]).to_a.map(&:id).sort - end - end - - def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges_and_nils - assert_deprecated do - assert_equal [1,3,4,5], Topic.where(parent_id: [[2..6], nil]).to_a.map(&:id).sort - end - end - - def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges_and_more_nils - assert_deprecated do - assert_equal [], Topic.where(parent_id: [[7..10, nil, [nil]], [nil]]).to_a.map(&:id).sort - end - end - def test_find_on_multiple_hash_conditions assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } @@ -802,7 +801,9 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_one_attribute_bang assert_equal topics(:first), Topic.find_by_title!("The First Topic") - assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } + assert_raises_with_message(ActiveRecord::RecordNotFound, "Couldn't find Topic") do + Topic.find_by_title!("The First Topic!") + end end def test_find_by_on_attribute_that_is_a_reserved_word @@ -920,7 +921,7 @@ class FinderTest < ActiveRecord::TestCase joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id'). where('project_id=1').to_a assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map { |d| d.name } + developer_names = developers_on_project_one.map(&:name) assert developer_names.include?('David') assert developer_names.include?('Jamis') end @@ -975,7 +976,7 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } + assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end @@ -1001,7 +1002,7 @@ class FinderTest < ActiveRecord::TestCase where(client_of: [2, 1, nil], name: ['37signals', 'Summit', 'Microsoft']). order('client_of DESC'). - map { |x| x.client_of } + map(&:client_of) assert client_of.include?(nil) assert_equal [2, 1].sort, client_of.compact.sort @@ -1011,7 +1012,7 @@ class FinderTest < ActiveRecord::TestCase client_of = Company. where(client_of: [nil]). order('client_of DESC'). - map { |x| x.client_of } + map(&:client_of) assert_equal [], client_of.compact end @@ -1071,6 +1072,11 @@ class FinderTest < ActiveRecord::TestCase assert_equal nil, Post.find_by("1 = 0") end + test "find_by with associations" do + assert_equal authors(:david), Post.find_by(author: authors(:david)).author + assert_equal authors(:mary) , Post.find_by(author: authors(:mary) ).author + end + test "find_by doesn't have implicit ordering" do assert_sql(/^((?!ORDER).)*$/) { Post.find_by(id: posts(:eager_other).id) } end @@ -1097,6 +1103,26 @@ class FinderTest < ActiveRecord::TestCase end end + test "find on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find(tyre.id) + assert_equal tyre2, zyke.tyres.custom_find(tyre2.id) + end + + test "find_by on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find_by(id: tyre.id) + assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) @@ -1113,4 +1139,10 @@ class FinderTest < ActiveRecord::TestCase end end) end + + def assert_raises_with_message(exception_class, message, &block) + err = assert_raises(exception_class) { block.call } + assert_match message, err.message + end + end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 385ec48005..7ef2ebc998 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -5,11 +5,13 @@ require 'models/admin/randomly_named_c1' require 'models/admin/user' require 'models/binary' require 'models/book' +require 'models/bulb' require 'models/category' require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' +require 'models/computer' require 'models/joke' require 'models/matey' require 'models/parrot' @@ -271,7 +273,7 @@ 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" @@ -290,6 +292,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, :class => parrot + pt.belongs_to :treasure, :class => treasure + + parrot.has_many :parrot_treasures, :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 @@ -670,12 +690,13 @@ 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 if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' require 'models/uuid_parent' require 'models/uuid_child' - fixtures :uuid_parents, :uuid_children + fixtures :uuid_parents, :uuid_children end def test_identifies_strings @@ -790,6 +811,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) @@ -806,6 +831,12 @@ 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 @@ -822,34 +853,15 @@ class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase end end -class FixtureLoadingTest < ActiveRecord::TestCase - def test_logs_message_for_failed_dependency_load - ActiveRecord::Base.logger.expects(:warn).twice - ActiveRecord::TestCase.try_to_load_dependency('does_not_exist') - end - - def test_does_not_logs_message_for_dependency_that_has_been_defined_with_set_fixture_class - ActiveRecord::TestCase.set_fixture_class unknown_dead_parrots: DeadParrot - ActiveRecord::Base.logger.expects(:warn).never - ActiveRecord::TestCase.try_to_load_dependency('unknown_dead_parrot') - end - - def test_does_not_logs_message_for_successful_dependency_load - ActiveRecord::TestCase.expects(:require_dependency).with('works_out_fine') - ActiveRecord::Base.logger.expects(:warn).never - ActiveRecord::TestCase.try_to_load_dependency('works_out_fine') - end -end - class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase ActiveRecord::FixtureSet.reset_cache 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' @@ -860,14 +872,27 @@ 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 + +class FixturesWithDefaultScopeTest < ActiveRecord::TestCase + fixtures :bulbs + + test "inserts fixtures excluded by a default scope" do + assert_equal 1, Bulb.count + assert_equal 2, Bulb.unscoped.count + end + + test "allows access to fixtures excluded by a default scope" do + assert_equal "special", bulbs(:special).name end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index be635aeef9..f2ba28a32f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -24,15 +24,15 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Enable raise errors in after_commit and after_rollback. -ActiveRecord::Base.raise_in_transactional_callbacks = true - # Connect to the database 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) && @@ -46,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 @@ -95,7 +95,7 @@ EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false def verify_default_timezone_config if Time.zone != EXPECTED_ZONE $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `Time.zone` was leaked. Expected: #{EXPECTED_ZONE} Got: #{Time.zone} @@ -103,7 +103,7 @@ def verify_default_timezone_config end if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `ActiveRecord::Base.default_timezone` was leaked. Expected: #{EXPECTED_DEFAULT_TIMEZONE} Got: #{ActiveRecord::Base.default_timezone} @@ -111,7 +111,7 @@ def verify_default_timezone_config end if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} Got: #{ActiveRecord::Base.time_zone_aware_attributes} @@ -124,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 @@ -136,19 +136,6 @@ def disable_extension!(extension, connection) connection.reconnect! end -unless ENV['FIXTURE_DEBUG'] - module ActiveRecord::TestFixtures::ClassMethods - def try_to_load_dependency_with_silence(*args) - old = ActiveRecord::Base.logger.level - ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR - try_to_load_dependency_without_silence(*args) - ActiveRecord::Base.logger.level = old - end - - alias_method_chain :try_to_load_dependency, :silence - end -end - require "cases/validations_repair_helper" class ActiveSupport::TestCase include ActiveRecord::TestFixtures diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 792950d24d..3268555cb8 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -22,7 +22,7 @@ class InheritanceTest < ActiveRecord::TestCase company = Company.first company = company.dup company.extend(Module.new { - def read_attribute(name) + def _read_attribute(name) return ' ' if name == 'type' super end @@ -121,6 +121,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 @@ -294,17 +300,17 @@ 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 con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(:includes => :firm).find(1) end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index dfb8a608cb..018b7b0d8f 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,8 +1,8 @@ -# encoding: utf-8 require 'cases/helper' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/owner' require 'models/pet' diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5a4b1fb919..848174df06 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -33,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once - p1.first_name = 'anika2' p1.save! @@ -217,10 +215,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 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 bd3dd29f4d..30c91dfdcb 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 @@ -82,7 +82,7 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array + assert array_column.array? end def test_create_table_with_array_column @@ -93,10 +93,29 @@ module ActiveRecord columns = connection.columns(:testings) array_column = columns.detect { |c| c.name == "foo" } - assert array_column.array + assert array_column.array? end end + def test_create_table_with_bigint + connection.create_table :testings do |t| + t.bigint :eight_int + end + columns = connection.columns(:testings) + eight = columns.detect { |c| c.name == "eight_int" } + + if current_adapter?(:OracleAdapter) + assert_equal 'NUMBER(8)', eight.sql_type + elsif current_adapter?(:SQLite3Adapter) + assert_equal 'bigint', eight.sql_type + else + assert_equal :integer, eight.type + assert_equal 8, eight.limit + end + ensure + connection.drop_table :testings + end + def test_create_table_with_limits connection.create_table :testings do |t| t.column :foo, :string, :limit => 255 @@ -176,32 +195,29 @@ module ActiveRecord end def test_create_table_with_timestamps_should_create_datetime_columns - # FIXME: Remove the silence when we change the default `null` behavior - ActiveSupport::Deprecation.silence do - connection.create_table table_name do |t| - t.timestamps - end + connection.create_table table_name do |t| + t.timestamps end created_columns = connection.columns(table_name) created_at_column = created_columns.detect {|c| c.name == 'created_at' } updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } - assert created_at_column.null - assert updated_at_column.null + assert !created_at_column.null + assert !updated_at_column.null end def test_create_table_with_timestamps_should_create_datetime_columns_with_options connection.create_table table_name do |t| - t.timestamps :null => false + t.timestamps null: true end created_columns = connection.columns(table_name) created_at_column = created_columns.detect {|c| c.name == 'created_at' } updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } - assert !created_at_column.null - assert !updated_at_column.null + assert created_at_column.null + assert updated_at_column.null end def test_create_table_without_a_block @@ -387,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| @@ -396,5 +423,36 @@ module ActiveRecord yield end end + + if ActiveRecord::Base.connection.supports_foreign_keys? + class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :trains + @connection.create_table(:wagons) { |t| t.references :train } + @connection.add_foreign_key :wagons, :trains + end + + teardown do + [:wagons, :trains].each do |table| + @connection.drop_table table, if_exists: true + end + end + + def test_create_table_with_force_cascade_drops_dependent_objects + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # can't re-create table referenced by foreign key + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_table :trains, force: true + end + + # can recreate referenced table with force: :cascade + @connection.create_table :trains, force: :cascade + assert_equal [], @connection.foreign_keys(:wagons) + end + end + end end end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 777a48ad14..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 @@ -95,8 +95,15 @@ module ActiveRecord def test_remove_timestamps_creates_updated_at_and_created_at with_change_table do |t| - @connection.expect :remove_timestamps, nil, [:delete_me] - t.remove_timestamps + @connection.expect :remove_timestamps, nil, [:delete_me, { null: true }] + t.remove_timestamps({ null: true }) + 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 @@ -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, {}] @@ -213,6 +246,12 @@ module ActiveRecord t.rename :bar, :baz end end + + def test_table_name_set + with_change_table do |t| + assert_equal :delete_me, t.name + end + end end end end diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 77a752f050..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 @@ -25,30 +25,30 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_column_positioning - assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first second third), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning conn.add_column :testings, :new_col, :integer - assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first second third new_col), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning_first conn.add_column :testings, :new_col, :integer, :first => true - assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning_after conn.add_column :testings, :new_col, :integer, :after => :first - assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name) end def test_change_column_with_positioning conn.change_column :testings, :second, :integer, :first => true - assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(second first third), conn.columns(:testings).map(&:name) conn.change_column :testings, :second, :integer, :after => :third - assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first third second), conn.columns(:testings).map(&:name) end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index e6aa901814..e5ccfe0f91 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -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 e955beae1a..3844b1a92e 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -237,8 +237,8 @@ module ActiveRecord end def test_invert_remove_timestamps - add = @recorder.inverse_of :remove_timestamps, [:table] - assert_equal [:add_timestamps, [:table], nil], add + add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }] + assert_equal [:add_timestamps, [:table, {null: true }], nil], add end def test_invert_add_reference @@ -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 diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 406dd70c37..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 @@ -17,11 +18,11 @@ module ActiveRecord setup do @connection = ActiveRecord::Base.connection - @connection.create_table "rockets" do |t| + @connection.create_table "rockets", force: true do |t| t.string :name end - @connection.create_table "astronauts" do |t| + @connection.create_table "astronauts", force: true do |t| t.string :name t.references :rocket 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 @@ -162,6 +184,14 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("astronauts") end + def test_remove_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: :rocket_id + assert_equal [], @connection.foreign_keys("astronauts") + end + def test_remove_foreign_key_by_name @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" @@ -212,6 +242,7 @@ module ActiveRecord ensure silence_stream($stdout) { migration.migrate(:down) } end + end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index ac932378fd..b23b9a679f 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -36,6 +36,20 @@ module ActiveRecord assert connection.index_name_exists?(table_name, 'new_idx', true) end + def test_rename_index_too_long + too_long_index_name = good_index_name + 'x' + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], :name => 'old_idx') + e = assert_raises(ArgumentError) { + connection.rename_index(table_name, 'old_idx', too_long_index_name) + } + assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) + + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + assert connection.index_name_exists?(table_name, 'old_idx', false) + end + + def test_double_add_index connection.add_index(table_name, [:foo], :name => 'some_idx') assert_raises(ArgumentError) { diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb new file mode 100644 index 0000000000..17ac72a109 --- /dev/null +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -0,0 +1,111 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end + + test "foreign keys can be created with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "no foreign key is created by default" do + @connection.create_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "options hash can be passed" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when creating the table" do + @connection.create_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign keys can be created while changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "foreign keys are not added by default when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys accept options when changing the table" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + 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 + end + end +end +end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 4485701a4e..ad6b828d0b 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -55,7 +55,7 @@ module ActiveRecord t.references :foo, :polymorphic => true, :index => true end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) end end @@ -93,7 +93,7 @@ module ActiveRecord t.references :foo, :polymorphic => true, :index => true end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index b8b4fa1135..988bd9c89f 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -42,7 +42,7 @@ module ActiveRecord def test_creates_polymorphic_index add_reference table_name, :taggable, polymorphic: true, index: true - assert index_exists?(table_name, [:taggable_id, :taggable_type]) + assert index_exists?(table_name, [:taggable_type, :taggable_id]) end def test_creates_reference_type_column_with_default diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index c8b3f75e10..3eef308428 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -39,33 +39,35 @@ module ActiveRecord end end - def test_rename_table - rename_table :test_models, :octopi + unless current_adapter?(:FbAdapter) # Firebird cannot rename tables + def test_rename_table + rename_table :test_models, :octopi - connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") - end + assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") + end - def test_rename_table_with_an_index - add_index :test_models, :url + def test_rename_table_with_an_index + add_index :test_models, :url - rename_table :test_models, :octopi + rename_table :test_models, :octopi - connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") - index = connection.indexes(:octopi).first - assert index.columns.include?("url") - assert_equal 'index_octopi_on_url', index.name - end + assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") + index = connection.indexes(:octopi).first + assert index.columns.include?("url") + assert_equal 'index_octopi_on_url', index.name + end - def test_rename_table_does_not_rename_custom_named_index - add_index :test_models, :url, name: 'special_url_idx' + def test_rename_table_does_not_rename_custom_named_index + add_index :test_models, :url, name: 'special_url_idx' - rename_table :test_models, :octopi + rename_table :test_models, :octopi - assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name) + assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name) + end end if current_adapter?(:PostgreSQLAdapter) @@ -84,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 270e446c69..3b73685a2c 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -5,6 +5,7 @@ require 'bigdecimal/util' require 'models/person' require 'models/topic' require 'models/developer' +require 'models/computer' require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" require MIGRATIONS_ROOT + "/rename/1_we_need_things" @@ -13,10 +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 :world_population, Type::Integer.new - attribute :my_house_population, Type::Integer.new + attribute :my_house_population, :integer end class Reminder < ActiveRecord::Base; end @@ -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 @@ -101,15 +101,24 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path end + def test_any_migrations + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid" + + assert ActiveRecord::Migrator.any_migrations? + + ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty" + + assert_not ActiveRecord::Migrator.any_migrations? + ensure + ActiveRecord::Migrator.migrations_paths = old_path + end + def test_migration_version ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) 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 @@ -120,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 @@ -149,6 +158,7 @@ class MigrationTest < ActiveRecord::TestCase assert !BigNumber.table_exists? GiveMeBigNumbers.up + BigNumber.reset_column_information assert BigNumber.create( :bank_balance => 1586.43, @@ -384,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 @@ -403,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 @@ -414,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 @@ -427,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? @@ -498,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| @@ -705,6 +717,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end class CopyMigrationsTest < ActiveRecord::TestCase + include ActiveSupport::Testing::Stream + def setup end @@ -914,13 +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 end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 9809d83315..c0daa83e9c 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -149,7 +149,7 @@ class MigratorTest < ActiveRecord::TestCase def test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] ActiveRecord::Migrator.new(:up, migrations).migrate - assert migrations.all? { |m| m.went_up } + assert migrations.all?(&:went_up) assert migrations.all? { |m| !m.went_down } assert_equal 2, ActiveRecord::Migrator.current_version end @@ -160,7 +160,7 @@ class MigratorTest < ActiveRecord::TestCase migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] ActiveRecord::Migrator.new(:down, migrations).migrate assert migrations.all? { |m| !m.went_up } - assert migrations.all? { |m| m.went_down } + assert migrations.all?(&:went_down) assert_equal 0, ActiveRecord::Migrator.current_version end @@ -357,11 +357,11 @@ class MigratorTest < ActiveRecord::TestCase end private - def m(name, version, &block) + def m(name, version) x = Sensor.new name, version x.extend(Module.new { - define_method(:up) { block.call(:up, x); super() } - define_method(:down) { block.call(:down, x); super() } + define_method(:up) { yield(:up, x); super() } + define_method(:down) { yield(:down, x); super() } }) if block_given? end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index e87773df94..6f65bf80eb 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/company_in_module' require 'models/shop' require 'models/developer' +require 'models/computer' class ModulesTest < ActiveRecord::TestCase fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants 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..f9bc266e84 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -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 cf96c3fccf..198cd6f341 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -13,7 +13,7 @@ require 'active_support/hash_with_indifferent_access' class TestNestedAttributesInGeneral < ActiveRecord::TestCase teardown do - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_base_should_have_an_empty_nested_attributes_options @@ -300,13 +300,13 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc(&:empty?) @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' }) assert_equal @ship, @pirate.reload.ship - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_should_also_work_with_a_HashWithIndifferentAccess @@ -494,12 +494,12 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false - Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } + Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?) @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' }) assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } ensure - Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_should_work_with_update_as_well @@ -672,7 +672,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveRecord::UnknownAttributeError do + assert_nothing_raised ActiveModel::AttributeAssignment::UnknownAttributeError do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end @@ -855,7 +855,7 @@ end module NestedAttributesLimitTests def teardown - Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_limit_with_less_records @@ -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 2170fe6118..2803ad2de0 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -8,6 +8,7 @@ require 'models/reply' require 'models/category' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/minimalistic' require 'models/warehouse_thing' @@ -126,7 +127,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_difference('Topic.count', -topics_by_mary.size) do destroyed = Topic.destroy_all(conditions).sort_by(&:id) assert_equal topics_by_mary, destroyed - assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen" + assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" end end @@ -136,7 +137,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) assert_equal clients, destroyed - assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" + assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" end end @@ -251,8 +252,10 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.instantiate( - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' + 'attributes' => { + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' + } ) assert_nothing_raised { topic.save } end @@ -353,6 +356,16 @@ 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_delete topic = Topic.find(1) assert_equal topic, topic.delete, 'topic.delete did not return self' @@ -877,4 +890,35 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Welcome to the weblog", post.title assert_not post.new_record? end + + class SaveTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_save_touch_false + widget = Class.new(ActiveRecord::Base) do + connection.create_table :widgets, force: true do |t| + t.string :name + t.timestamps null: false + end + + self.table_name = :widgets + end + + instance = widget.create!({ + name: 'Bob', + created_at: 1.day.ago, + updated_at: 1.day.ago + }) + + created_at = instance.created_at + updated_at = instance.updated_at + + instance.name = 'Barb' + instance.save!(touch: false) + assert_equal instance.created_at, created_at + assert_equal instance.updated_at, updated_at + ensure + ActiveRecord::Base.connection.drop_table :widgets + end + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 8eea10143f..287a3f33ea 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -13,7 +13,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase teardown do ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) - @per_test_teardown.each {|td| td.call } + @per_test_teardown.each(&:call) end # Will deadlock due to lack of Monitor timeouts in 1.9 @@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase end end + def checkout_checkin_connections_loop(pool_size, loops) + ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) + @connection_count = 0 + @timed_out = 0 + loops.times do + begin + conn = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + ActiveRecord::Base.connection.tables + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end + end + def test_pooled_connection_checkin_one checkout_checkin_connections 1, 2 assert_equal 2, @connection_count @@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase assert_equal 1, ActiveRecord::Base.connection_pool.connections.size end + def test_pooled_connection_checkin_two + checkout_checkin_connections_loop 2, 3 + assert_equal 3, @connection_count + assert_equal 0, @timed_out + assert_equal 2, ActiveRecord::Base.connection_pool.connections.size + end + + def test_pooled_connection_remove + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5})) + old_connection = ActiveRecord::Base.connection + extra_connection = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.remove(extra_connection) + assert_equal ActiveRecord::Base.connection, old_connection + end private diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f19a6ea5e3..1ea1ef5e12 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'support/schema_dumping_helper' require 'models/topic' require 'models/reply' require 'models/subscriber' @@ -195,6 +196,37 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_fixtures = false + + class Barcode < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) + end + + teardown do + @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + end + + def test_any_type_primary_key + assert_equal "code", Barcode.primary_key + + column_type = Barcode.type_for_attribute(Barcode.primary_key) + assert_equal :string, column_type.type + assert_equal 42, column_type.limit + end + + test "schema dump primary key includes type and options" do + schema = dump_table_schema "barcodes" + assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -209,8 +241,10 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter) +if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false class Widget < ActiveRecord::Base @@ -218,19 +252,45 @@ if current_adapter?(:PostgreSQLAdapter) setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, id: :bigserial) { |t| } + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table(:widgets, id: :bigserial, force: true) + else + @connection.create_table(:widgets, id: :bigint, force: true) + end end teardown do - @connection.drop_table :widgets + @connection.drop_table 'widgets', if_exists: true end - def test_bigserial_primary_key - assert_equal "id", Widget.primary_key - assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + test "primary key column type with bigserial" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type + assert_equal 8, column_type.limit + end + test "primary key with bigserial are automatically numbered" do widget = Widget.create! assert_not_nil widget.id end + + test "schema dump primary key with bigserial" do + schema = dump_table_schema "widgets" + if current_adapter?(:PostgreSQLAdapter) + assert_match %r{create_table "widgets", id: :bigserial}, schema + else + 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 9d89d6a1e8..744f9edc47 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -212,6 +212,38 @@ class QueryCacheTest < ActiveRecord::TestCase ensure ActiveRecord::Base.configurations = conf end + + def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries + ActiveRecord::Base.connection.enable_query_cache! + post = Post.first + + Post.transaction do + post.update_attributes(title: 'rollback') + assert_equal 1, Post.where(title: 'rollback').to_a.count + raise ActiveRecord::Rollback + end + + assert_equal 0, Post.where(title: 'rollback').to_a.count + + ActiveRecord::Base.connection.uncached do + assert_equal 0, Post.where(title: 'rollback').to_a.count + end + + begin + Post.transaction do + post.update_attributes(title: 'rollback') + assert_equal 1, Post.where(title: 'rollback').to_a.count + raise 'broken' + end + rescue Exception + end + + assert_equal 0, Post.where(title: 'rollback').to_a.count + + ActiveRecord::Base.connection.uncached do + assert_equal 0, Post.where(title: 'rollback').to_a.count + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase 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/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 2afd25c989..1c919f0b57 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -3,6 +3,7 @@ require 'models/author' require 'models/post' require 'models/comment' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/reader' require 'models/person' @@ -22,9 +23,15 @@ class ReadOnlyTest < ActiveRecord::TestCase assert !dev.save dev.name = 'Forbidden.' end - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } + assert_equal "Developer is marked as readonly", e.message + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + assert_equal "Developer is marked as readonly", e.message + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } + assert_equal "Developer is marked as readonly", e.message end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index f52fd22489..cccfc6774e 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -60,7 +60,7 @@ module ActiveRecord def test_connection_pool_starts_reaper spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 0.0001 + spec.config[:reaping_frequency] = '0.0001' pool = ConnectionPool.new spec diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 84abaf0291..67e9bef808 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -50,13 +50,13 @@ class ReflectionTest < ActiveRecord::TestCase end def test_columns_are_returned_in_the_order_they_were_declared - column_names = Topic.columns.map { |column| column.name } + column_names = Topic.columns.map(&:name) assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names end def test_content_columns content_columns = Topic.content_columns - content_column_names = content_columns.map {|column| column.name} + content_column_names = content_columns.map(&:name) assert_equal 13, content_columns.length assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort end @@ -80,10 +80,21 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end - def test_non_existent_columns_return_nil - assert_deprecated do - assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") - end + def test_non_existent_columns_return_null_object + column = @first.column_for_attribute("attribute_that_doesnt_exist") + assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column + assert_equal "attribute_that_doesnt_exist", column.name + assert_equal nil, column.sql_type + assert_equal nil, column.type + end + + def test_non_existent_types_are_identity_types + type = @first.type_for_attribute("attribute_that_doesnt_exist") + object = Object.new + + 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 @@ -209,6 +220,10 @@ class ReflectionTest < ActiveRecord::TestCase assert_not_equal Object.new, Firm._reflections['clients'] end + def test_reflections_should_return_keys_as_strings + assert Category.reflections.keys.all? { |key| key.is_a? String }, "Model.reflections is expected to return string for keys" + end + def test_has_and_belongs_to_many_reflection assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 8f40a1890d..0a2e874e4f 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -2,6 +2,7 @@ require 'cases/helper' require 'models/author' require 'models/comment' require 'models/developer' +require 'models/computer' require 'models/post' require 'models/project' require 'models/rating' @@ -81,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 4c94c2fd0d..45ead08bd5 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def relation - @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| @@ -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]).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 @@ -99,7 +99,7 @@ module ActiveRecord end test '#reorder!' do - relation = self.relation.order('foo') + @relation = self.relation.order('foo') assert relation.reorder!('bar').equal?(relation) assert_equal ['bar'], relation.order_values @@ -116,7 +116,7 @@ module ActiveRecord end test 'reverse_order!' do - relation = Post.order('title ASC, comments_count DESC') + @relation = Post.order('title ASC, comments_count DESC') relation.reverse_order! @@ -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 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 4057835688..8f62014622 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -4,11 +4,13 @@ require 'models/topic' module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers - PredicateBuilder.register_handler(Regexp, proc do |column, value| + Topic.predicate_builder.register_handler(Regexp, proc do |column, value| 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 end end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index b9e69bdb08..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,119 +24,82 @@ 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 + assert_equal expected.where_clause, relation.where_clause + end - 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 + 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') - 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 + assert_equal expected.where_clause, relation.where_clause 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 + def test_rewhere_with_range + relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5) + + assert_equal Post.where(comments_count: 3..5), relation end - def test_rewhere_with_one_overwriting_condition_and_one_unrelated - relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') + def test_rewhere_with_infinite_upper_bound_range + relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5) - assert_equal 2, relation.where_values.size + assert_equal Post.where(comments_count: 3..5), relation + end - value = relation.where_values.first - bind = relation.bind_values.first + def test_rewhere_with_infinite_lower_bound_range + relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5) - assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality - assert_equal 'world', bind.last + assert_equal Post.where(comments_count: 3..5), relation + end - value = relation.where_values.second - bind = relation.bind_values.second + def test_rewhere_with_infinite_range + relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5) - assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality - assert_equal 'alone', bind.last + assert_equal Post.where(comments_count: 3..5), relation end 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 39c8afdd23..6af31017d6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -1,16 +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/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) @@ -25,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 @@ -180,12 +202,6 @@ module ActiveRecord assert_equal 0, Post.where(:id => []).count end - def test_where_with_table_name_and_nested_empty_array - assert_deprecated do - assert_equal [], Post.where(:id => [[]]).to_a - end - end - def test_where_with_empty_hash_and_no_foreign_key assert_equal 0, Edge.where(:sink => {}).count end @@ -225,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 3280945d09..9353be1ba7 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -23,19 +23,19 @@ module ActiveRecord end def test_construction - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' end def test_responds_to_model_and_returns_klass - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -43,19 +43,19 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| assert_equal [], relation.send("#{method}_values"), method.to_s end end def test_extensions - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) relation.where! :hello @@ -63,19 +63,20 @@ module ActiveRecord end def test_has_values - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right @@ -84,24 +85,25 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('posts'), :b + relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder) assert_equal 'posts', relation.table_name end def test_scope_for_create - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) hash = { :hello => 'world' } relation.create_with_value = hash assert_equal hash, relation.scope_for_create end def test_create_with_value_with_wheres - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) @@ -109,9 +111,10 @@ module ActiveRecord # FIXME: is this really wanted or expected behavior? def test_scope_for_create_is_cached - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) @@ -126,62 +129,72 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert !relation.eager_loading? end def test_eager_load_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation.eager_load! :b assert relation.eager_loading? end def test_references_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ['foo', 'omg', 'lol'], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.references(:foo).references(:foo) assert_equal ['foo'], relation.references_values end test 'merging a hash into a relation' do - relation = Relation.new FakeKlass, :b + 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).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).where! :foo + 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, where: [:foo]) - assert_equal [:foo], relation.where_values - end - - test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b) - 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 @@ -192,13 +205,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b) + 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 - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value @@ -241,12 +254,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 diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 410b5ba5a3..0cf44388fa 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -7,6 +7,7 @@ require 'models/comment' require 'models/author' require 'models/entrant' require 'models/developer' +require 'models/computer' require 'models/reply' require 'models/company' require 'models/bird' @@ -15,12 +16,18 @@ require 'models/engine' require 'models/tyre' require 'models/minivan' require 'models/aircraft' +require "models/possession" class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :tags, :taggings, :cars, :minivans + class TopicWithCallbacks < ActiveRecord::Base + self.table_name = :topics + before_update { |topic| topic.author_name = 'David' if topic.author_name.blank? } + end + def test_do_not_double_quote_string_id van = Minivan.last assert van @@ -33,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 @@ -159,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) @@ -248,7 +257,7 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_reorder topics = Topic.order('author_name').order('title').reorder('id').to_a - topics_titles = topics.map{ |t| t.title } + topics_titles = topics.map(&:title) assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles end @@ -344,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 @@ -352,7 +363,7 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil, {}) + assert_equal 0, Developer.none.calculate(:count, nil) assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -440,7 +451,7 @@ class RelationTest < ActiveRecord::TestCase where('project_id=1').to_a assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map { |d| d.name } + developer_names = developers_on_project_one.map(&:name) assert developer_names.include?('David') assert developer_names.include?('Jamis') end @@ -651,8 +662,8 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } - assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) + assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end authors = Author.all @@ -711,7 +722,9 @@ class RelationTest < ActiveRecord::TestCase def test_find_by_classname Author.create!(:name => Mary.name) - assert_equal 1, Author.where(:name => Mary).size + assert_deprecated do + assert_equal 1, Author.where(:name => Mary).size + end end def test_find_by_id_with_list_of_ar @@ -849,6 +862,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 @@ -1096,6 +1115,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 @@ -1374,12 +1425,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal "id", Post.all.primary_key end - def test_disable_implicit_join_references_is_deprecated - assert_deprecated do - ActiveRecord::Base.disable_implicit_join_references = true - end - end - def test_ordering_with_extra_spaces assert_equal authors(:david), Author.order('id DESC , name DESC').last end @@ -1426,6 +1471,19 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end + def test_update_on_relation + topic1 = TopicWithCallbacks.create! title: 'arel', author_name: nil + topic2 = TopicWithCallbacks.create! title: 'activerecord', author_name: nil + topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) + topics.update(title: 'adequaterecord') + + assert_equal 'adequaterecord', topic1.reload.title + assert_equal 'adequaterecord', topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal 'David', topic1.reload.author_name + assert_equal 'David', topic2.reload.author_name + end + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1447,10 +1505,31 @@ class RelationTest < ActiveRecord::TestCase 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 @@ -1636,6 +1715,14 @@ class RelationTest < ActiveRecord::TestCase end end + test "relations with cached arel can't be mutated [internal API]" do + relation = Post.all + relation.count + + assert_raises(ActiveRecord::ImmutableRelation) { relation.limit!(5) } + assert_raises(ActiveRecord::ImmutableRelation) { relation.where!("1 = 2") } + end + test "relations show the records in #inspect" do relation = Post.limit(2) assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect @@ -1660,7 +1747,9 @@ class RelationTest < ActiveRecord::TestCase test 'using a custom table affects the wheres' do table_alias = Post.arel_table.alias('omg_posts') - relation = ActiveRecord::Relation.new Post, table_alias + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) relation.where!(:foo => "bar") node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first @@ -1702,7 +1791,7 @@ class RelationTest < ActiveRecord::TestCase end def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new('?')) + left = Post.where(id: Arel::Nodes::BindParam.new) column = Post.columns_hash['id'] left.bind_values += [[column, 20]] @@ -1719,14 +1808,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/result_test.rb b/activerecord/test/cases/result_test.rb index d6decafad9..dec01dfa76 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,6 +10,10 @@ module ActiveRecord ]) end + test "length" do + assert_equal 3, result.length + end + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index dca85fb5eb..262e0abc22 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -7,15 +7,6 @@ class SanitizeTest < ActiveRecord::TestCase def setup end - def test_sanitize_sql_hash_handles_associations - quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name") - quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals") - expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" - - assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) - end - def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index d7ee56374d..513f65f707 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -19,7 +19,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_dump_schema_information_outputs_lexically_ordered_versions versions = %w{ 20100101010101 20100201010101 20100301010101 } - versions.reverse.each do |v| + versions.reverse_each do |v| ActiveRecord::SchemaMigration.create!(:version => v) end @@ -40,6 +40,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "schema_migrations"}, output end + def test_schema_dump_uses_force_cascade_on_create_table + output = dump_table_schema "authors" + assert_match %r{create_table "authors", force: :cascade}, output + end + def test_schema_dump_excludes_sqlite_sequence output = standard_dump assert_no_match %r{create_table "sqlite_sequence"}, output @@ -68,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(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/) match[0].length end - end + end.compact assert_equal 1, lengths.uniq.length end @@ -162,16 +167,6 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "schema_migrations"}, output end - def test_schema_dump_illegal_ignored_table_value - stream = StringIO.new - old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, [5] - assert_raise(StandardError) do - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - end - ensure - ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables - end - def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) @@ -209,25 +204,30 @@ 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_does_not_include_limit_for_emulated_mysql_boolean_fields + output = standard_dump + assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output end def test_schema_dumps_index_type @@ -239,13 +239,13 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_decimal_options output = dump_all_table_schema([/^[^n]/]) - assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output + assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2\.78}, output end if current_adapter?(:PostgreSQLAdapter) 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 if ActiveRecord::Base.connection.supports_extensions? @@ -263,93 +263,17 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{enable_extension}, output end end - - def test_schema_dump_includes_xml_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_xml_data_type"} =~ output - assert_match %r{t.xml "data"}, output - end - end - - def test_schema_dump_includes_json_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_json_data_type"} =~ output - assert_match %r|t.json "json_data", default: {}|, output - end - end - - def test_schema_dump_includes_inet_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output - end - end - - def test_schema_dump_includes_cidr_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output - end - end - - def test_schema_dump_includes_macaddr_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output - end - end - - def test_schema_dump_includes_uuid_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_uuids"} =~ output - assert_match %r{t.uuid "guid"}, output - end - end - - def test_schema_dump_includes_hstores_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_hstores"} =~ output - assert_match %r[t.hstore "hash_store", default: {}], output - end - end - - def test_schema_dump_includes_citext_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_citext"} =~ output - assert_match %r[t.citext "text_citext"], output - end - end - - def test_schema_dump_includes_ltrees_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_ltrees"} =~ output - assert_match %r[t.ltree "path"], output - end - end - - def test_schema_dump_includes_arrays_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_arrays"} =~ output - assert_match %r[t.text\s+"nicknames",\s+array: true], output - assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output - end - end - - def test_schema_dump_includes_tsvector_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_tsvectors"} =~ output - assert_match %r{t.tsvector "text_vector"}, output - end - end end def test_schema_dump_keeps_large_precision_integer_columns_as_decimal 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 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 @@ -358,7 +282,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 @@ -371,6 +295,11 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) end + + def test_do_not_dump_foreign_keys_for_ignored_tables + output = dump_table_schema "authors" + assert_equal ["authors"], output.scan(/^\s*add_foreign_key "([^"]+)".+$/).flatten + end end class CreateDogMigration < ActiveRecord::Migration @@ -431,7 +360,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 a5c4404175..4137b20c4a 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -2,13 +2,14 @@ require 'cases/helper' require 'models/post' require 'models/comment' require 'models/developer' +require 'models/computer' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments def test_default_scope - expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } + expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect(&:salary) + received = DeveloperOrderedBySalary.all.collect(&:salary) assert_equal expected, received end @@ -85,14 +86,14 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_scope_overwrites_default - expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name } + expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect(&:name) + received = DeveloperOrderedBySalary.by_name.to_a.collect(&:name) assert_equal expected, received end def test_reorder_overrides_default_scope_order - expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + expected = Developer.order('name DESC').collect(&:name) + received = DeveloperOrderedBySalary.reorder('name DESC').collect(&:name) assert_equal expected, received end @@ -142,37 +143,45 @@ class DefaultScopingTest < ActiveRecord::TestCase expected_5 = Developer.order('salary DESC').collect(&:name) received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) assert_equal expected_5, received_5 + + expected_6 = Developer.order('salary DESC').collect(&:name) + received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq('David')).unscope(where: :name).collect(&:name) + assert_equal expected_6, received_6 + + expected_7 = Developer.order('salary DESC').collect(&:name) + received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq('David')).unscope(where: :name).collect(&:name) + assert_equal expected_7, received_7 end def test_unscope_multiple_where_clauses - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name) assert_equal expected, received end def test_unscope_string_where_clauses_involved dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago) - expected = dev_relation.collect { |dev| dev.name } + expected = dev_relation.collect(&:name) dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago) - received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.name } + received = dev_ordered_relation.unscope(where: [:name]).collect(&:name) assert_equal expected, received end def test_unscope_with_grouping_attributes - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name) assert_equal expected, received - expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } - received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name } + expected_2 = Developer.order('salary DESC').collect(&:name) + received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name) assert_equal expected_2, received_2 end def test_unscope_with_limit_in_query - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name) assert_equal expected, received end @@ -182,42 +191,42 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_reverse_order - expected = Developer.all.collect { |dev| dev.name } - received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.order('salary DESC').reverse_order.unscope(:order).collect(&:name) assert_equal expected, received end def test_unscope_select - expected = Developer.order('salary ASC').collect { |dev| dev.name } - received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect { |dev| dev.name } + expected = Developer.order('salary ASC').collect(&:name) + received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect(&:name) assert_equal expected, received - expected_2 = Developer.all.collect { |dev| dev.id } - received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id } + expected_2 = Developer.all.collect(&:id) + received_2 = Developer.select(:name).unscope(:select).collect(&:id) assert_equal expected_2, received_2 end def test_unscope_offset - expected = Developer.all.collect { |dev| dev.name } - received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.offset(5).unscope(:offset).collect(&:name) assert_equal expected, received end def test_unscope_joins_and_select_on_developers_projects - expected = Developer.all.collect { |dev| dev.name } - received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect(&:name) assert_equal expected, received end def test_unscope_includes - expected = Developer.all.collect { |dev| dev.name } - received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name) assert_equal expected, received end def test_unscope_having - expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name } - received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name } + expected = DeveloperOrderedBySalary.all.collect(&:name) + received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect(&:name) assert_equal expected, received end @@ -275,13 +284,13 @@ 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 - expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary } + expected = Developer.all.merge!(order: 'salary desc').to_a.collect(&:salary) + received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect(&:salary) assert_equal expected, received end @@ -417,19 +426,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 d3546bd471..e4cc533517 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -5,6 +5,7 @@ require 'models/comment' require 'models/reply' require 'models/author' require 'models/developer' +require 'models/computer' class NamedScopingTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses @@ -132,6 +133,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5) end + def test_scopes_body_is_a_callable + e = assert_raises ArgumentError do + Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") } + end + assert_equal "The scope body needs to be callable.", e.message + end + def test_active_records_have_scope_named__all__ assert !Topic.all.empty? @@ -309,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| @@ -372,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 8e512e118a..4bfffbe9c6 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/post' require 'models/author' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/comment' require 'models/category' @@ -15,6 +16,26 @@ class RelationScopingTest < ActiveRecord::TestCase developers(:david) end + def test_unscoped_breaks_caching + author = authors :mary + assert_nil author.first_post + post = FirstPost.unscoped do + author.reload.first_post + end + assert post + end + + def test_scope_breaks_caching_on_collections + author = authors :david + ids = author.reload.special_posts_with_default_scope.map(&:id) + assert_equal [1,5,6], ids.sort + scoped_posts = SpecialPostWithDefaultScope.unscoped do + author = authors :david + author.reload.special_posts_with_default_scope.to_a + end + assert_equal author.posts.map(&:id).sort, scoped_posts.map(&:id).sort + end + def test_reverse_order assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end @@ -163,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 @@ -187,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 new file mode 100644 index 0000000000..e731443fc2 --- /dev/null +++ b/activerecord/test/cases/secure_token_test.rb @@ -0,0 +1,32 @@ +require 'cases/helper' +require 'models/user' + +class SecureTokenTest < ActiveRecord::TestCase + setup do + @user = User.new + end + + def test_token_values_are_generated_for_specified_attributes_and_persisted_on_save + @user.save + assert_not_nil @user.token + assert_not_nil @user.auth_token + end + + def test_regenerating_the_secure_token + @user.save + old_token = @user.token + old_auth_token = @user.auth_token + @user.regenerate_token + @user.regenerate_auth_token + + assert_not_equal @user.token, old_token + assert_not_equal @user.auth_token, old_auth_token + end + + def test_token_value_not_overwritten_when_present + @user.token = "custom-secure-token" + @user.save + + assert_equal @user.token, "custom-secure-token" + end +end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 3f52e80e11..35b13ea247 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -2,6 +2,8 @@ require "cases/helper" require 'models/contact' require 'models/topic' require 'models/book' +require 'models/author' +require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books @@ -92,4 +94,11 @@ class SerializationTest < ActiveRecord::TestCase book = klazz.find(books(:awdr).id) assert_equal 'paperback', book.read_attribute_for_serialization(:format) end + + def test_find_records_by_serialized_attributes_through_join + author = Author.create!(name: "David") + author.serialized_posts.create!(title: "Hello") + + assert_equal 1, Author.joins(:serialized_posts).where(name: "David", serialized_posts: { title: "Hello" }).length + end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index c5fb491b10..e29f7462c8 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -22,12 +22,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase end end - def test_list_of_serialized_attributes - assert_deprecated do - assert_equal %w(content), Topic.serialized_attributes.keys - end - end - def test_serialized_attribute Topic.serialize("content", MyObject) @@ -140,11 +134,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal 1, Topic.where(:content => nil).count end - def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type + def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type Topic.serialize(:content, Hash) assert_raise(ActiveRecord::SerializationTypeMismatch) do - topic = Topic.new(content: 'string') - topic.save + Topic.new(content: 'string') end end @@ -244,8 +237,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase t = Topic.create(content: "first") assert_equal("first", t.content) - t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) - assert_equal("second", t.content) + t.update_column(:content, ["second"]) + assert_equal(["second"], t.content) + assert_equal(["second"], t.reload.content) end def test_serialized_column_should_unserialize_after_update_attribute @@ -254,5 +248,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase t.update_attribute(:content, "second") assert_equal("second", t.content) + assert_equal("second", t.reload.content) + end + + def test_nil_is_not_changed_when_serialized_with_a_class + Topic.serialize(:content, Array) + + topic = Topic.new(content: nil) + + assert_not topic.content_changed? + end + + def test_classes_without_no_arg_constructors_are_not_supported + assert_raises(ArgumentError) do + Topic.serialize(:content, Regexp) + end end end diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb new file mode 100644 index 0000000000..1c449d42fe --- /dev/null +++ b/activerecord/test/cases/suppressor_test.rb @@ -0,0 +1,21 @@ +require 'cases/helper' +require 'models/notification' +require 'models/user' + +class SuppressorTest < ActiveRecord::TestCase + def test_suppresses_creation_of_record_generated_by_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/test_case.rb b/activerecord/test/cases/test_case.rb index eb44c4a83c..e0b01ae8e0 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 @@ -43,7 +29,7 @@ module ActiveRecord patterns_to_match.each do |pattern| failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end def assert_queries(num = 1, options = {}) diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb new file mode 100644 index 0000000000..ff4e5ecec5 --- /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_fixtures = 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 abf6becc17..c0c62527df 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'support/ddl_helper' require 'models/developer' +require 'models/computer' require 'models/owner' require 'models/pet' require 'models/toy' @@ -72,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) @@ -90,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 diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index b5ac1bdaf9..e868022fed 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 @@ -266,47 +265,6 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal 2, @first.rollbacks end - def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called - old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks - ActiveRecord::Base.raise_in_transactional_callbacks = false - - def @first.last_after_transaction_error=(e); @last_transaction_error = e; end - def @first.last_after_transaction_error; @last_transaction_error; end - @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} - @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} - - second = TopicWithCallbacks.find(3) - second.after_commit_block{|r| r.history << :after_commit} - second.after_rollback_block{|r| r.history << :after_rollback} - - Topic.transaction do - @first.save! - second.save! - end - assert_equal :commit, @first.last_after_transaction_error - assert_equal [:after_commit], second.history - - second.history.clear - Topic.transaction do - @first.save! - second.save! - raise ActiveRecord::Rollback - end - assert_equal :rollback, @first.last_after_transaction_error - assert_equal [:after_rollback], second.history - ensure - ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config - end - - def test_after_commit_should_not_raise_when_raise_in_transactional_callbacks_false - old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks - ActiveRecord::Base.raise_in_transactional_callbacks = false - @first.after_commit_block{ fail "boom" } - Topic.transaction { @first.save! } - ensure - ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config - end - def test_after_commit_callback_should_not_swallow_errors @first.after_commit_block{ fail "boom" } assert_raises(RuntimeError) do @@ -371,10 +329,14 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def test_after_rollback_callbacks_should_validate_on_condition assert_raise(ArgumentError) { Topic.after_rollback(on: :save) } + e = assert_raise(ArgumentError) { Topic.after_rollback(on: 'create') } + assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message) end def test_after_commit_callbacks_should_validate_on_condition assert_raise(ArgumentError) { Topic.after_commit(on: :save) } + e = assert_raise(ArgumentError) { Topic.after_commit(on: 'create') } + assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message) end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object @@ -438,3 +400,63 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase assert_equal [:update_and_destroy, :create_and_destroy], topic.history 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/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 9cfe041de2..88e595c39f 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/topic' require 'models/reply' require 'models/developer' +require 'models/computer' require 'models/book' require 'models/author' require 'models/post' @@ -12,7 +13,7 @@ class TransactionTest < ActiveRecord::TestCase fixtures :topics, :developers, :authors, :posts def setup - @first, @second = Topic.find(1, 2).sort_by { |t| t.id } + @first, @second = Topic.find(1, 2).sort_by(&:id) end def test_persisted_in_a_model_with_custom_primary_key_after_failed_save @@ -193,6 +194,16 @@ class TransactionTest < ActiveRecord::TestCase assert_equal posts_count, author.posts(true).size end + def test_cancellation_from_returning_false_in_before_filter + def @first.before_save_for_transaction + false + end + + assert_deprecated do + @first.save + end + end + def test_cancellation_from_before_destroy_rollbacks_in_destroy add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count @@ -492,6 +503,34 @@ class TransactionTest < ActiveRecord::TestCase assert topic.frozen?, 'not frozen' end + def test_rollback_when_thread_killed + return if in_memory_db? + + queue = Queue.new + thread = Thread.new do + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + + queue.push nil + sleep + + @second.save + end + end + + queue.pop + thread.kill + thread.join + + assert @first.approved?, "First should still be changed in the objects" + assert !@second.approved?, "Second should still be changed in the objects" + + assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert Topic.find(2).approved?, "Second should still be approved" + end + def test_restore_active_record_state_for_all_records_in_a_transaction topic_without_callbacks = Class.new(ActiveRecord::Base) do self.table_name = 'topics' @@ -530,6 +569,21 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end + def test_restore_frozen_state_after_double_destroy + topic = Topic.create + reply = topic.replies.create + + Topic.transaction do + topic.destroy # calls #destroy on reply (since dependent: destroy) + reply.destroy + + raise ActiveRecord::Rollback + end + + assert_not reply.frozen? + assert_not topic.frozen? + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) @@ -596,6 +650,27 @@ class TransactionTest < ActiveRecord::TestCase assert transaction.state.committed? end + def test_transaction_rollback_with_primarykeyless_tables + connection = ActiveRecord::Base.connection + connection.create_table(:transaction_without_primary_keys, force: true, id: false) do |t| + t.integer :thing_id + end + + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'transaction_without_primary_keys' + after_commit { } # necessary to trigger the has_transactional_callbacks branch + end + + assert_no_difference(-> { klass.count }) do + ActiveRecord::Base.transaction do + klass.create! + raise ActiveRecord::Rollback + end + end + ensure + connection.drop_table 'transaction_without_primary_keys', if_exists: true + end + private %w(validation save destroy).each do |filter| @@ -603,7 +678,7 @@ class TransactionTest < ActiveRecord::TestCase meta = class << topic; self; end meta.send("define_method", "before_#{filter}_for_transaction") do Book.create - false + throw(:abort) end end end @@ -667,7 +742,7 @@ if current_adapter?(:PostgreSQLAdapter) end end - threads.each { |t| t.join } + threads.each(&:join) end end @@ -715,7 +790,7 @@ if current_adapter?(:PostgreSQLAdapter) Developer.connection.close end - threads.each { |t| t.join } + threads.each(&:join) end assert_equal original_salary, Developer.find(1).salary 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 da30de373e..fe49d0e79a 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -5,24 +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.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 @@ -31,7 +36,15 @@ 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? + type = Decimal.new + + assert type.changed?(5.0, 5.0, '5.0wibble') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(-5.0, -5.0, '-5.0') end end end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb new file mode 100644 index 0000000000..84fb05dd8e --- /dev/null +++ b/activerecord/test/cases/type/integer_test.rb @@ -0,0 +1,127 @@ +require "cases/helper" +require "models/company" + +module ActiveRecord + module Type + class IntegerTest < ActiveRecord::TestCase + test "simple values" do + type = Type::Integer.new + 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.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.cast(firm) + end + + test "casting objects without to_i" do + type = Type::Integer.new + assert_nil type.cast(::Object.new) + end + + test "casting nan and infinity" do + type = Type::Integer.new + 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.serialize(true) + assert_equal 0, type.serialize(false) + end + + test "changed?" do + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') + assert_not type.changed?(nil, nil, nil) + end + + test "values below int min value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-2147483649) + end + end + + test "values above int max value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(2147483648) + end + end + + test "very small numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-9999999999999999999999999999999) + end + end + + test "very large numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(9999999999999999999999999999999) + end + end + + test "normal numbers are in range" do + type = Integer.new + 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.serialize(2147483647)) + end + + test "int min value is in range" do + 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.serialize(9223372036854775807)) + assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) + assert_raises(::RangeError) do + type.serialize(-9999999999999999999999999999999) + end + assert_raises(::RangeError) do + 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 420177ed49..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 "1", type.type_cast_from_user(true) - assert_equal "0", 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/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb index 4e32f92dd0..172c6dfc4c 100644 --- a/activerecord/test/cases/type/type_map_test.rb +++ b/activerecord/test/cases/type/type_map_test.rb @@ -124,6 +124,53 @@ module ActiveRecord assert_equal mapping.lookup(3), 'string' assert_kind_of Type::Value, mapping.lookup(4) end + + def test_fetch + mapping = TypeMap.new + mapping.register_type(1, "string") + + assert_equal "string", mapping.fetch(1) { "int" } + assert_equal "int", mapping.fetch(2) { "int" } + end + + def test_fetch_yields_args + mapping = TypeMap.new + + assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") } + assert_equal "bar-1-2-3", mapping.fetch("bar", 1, 2, 3) { |*args| args.join("-") } + end + + def test_fetch_memoizes + mapping = TypeMap.new + + looked_up = false + mapping.register_type(1) do + fail if looked_up + looked_up = true + "string" + end + + assert_equal "string", mapping.fetch(1) + assert_equal "string", mapping.fetch(1) + end + + def test_fetch_memoizes_on_args + mapping = TypeMap.new + mapping.register_type("foo") { |*args| args.join("-") } + + assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") } + assert_equal "foo-2-3-4", mapping.fetch("foo", 2, 3, 4) { |*args| args.join("-") } + end + + def test_register_clears_cache + mapping = TypeMap.new + + mapping.register_type(1, "string") + mapping.lookup(1) + mapping.register_type(1, "int") + + assert_equal "int", mapping.lookup(1) + end end end end diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb new file mode 100644 index 0000000000..f2c910eade --- /dev/null +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" + +module ActiveRecord + module Type + class UnsignedIntegerTest < ActiveRecord::TestCase + test "unsigned int max value is in range" do + assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) + end + + test "minus value is out of range" do + assert_raises(::RangeError) do + UnsignedInteger.new.serialize(-1) + end + end + 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 db4f78d354..9b1859c2ce 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -1,89 +1,42 @@ require "cases/helper" -require 'models/company' module ActiveRecord module ConnectionAdapters 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.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.type_cast_from_user(' ') - assert_equal false, type.type_cast_from_user("\u3000\r\n") - assert_equal false, type.type_cast_from_user("\u0000") - assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') - end - - def test_type_cast_integer - 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) - end - - def test_type_cast_non_integer_to_integer - 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)) - end - - def test_type_cast_activerecord_to_integer - type = Type::Integer.new - firm = Firm.create(:name => 'Apple') - assert_nil type.type_cast_from_user(firm) - end - - def test_type_cast_object_without_to_i_to_integer - type = Type::Integer.new - assert_nil type.type_cast_from_user(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - type = Type::Integer.new - assert_nil type.type_cast_from_user(Float::NAN) - assert_nil type.type_cast_from_user(1.0/0.0) - end - - def test_changing_integers - type = Type::Integer.new - - assert type.changed?(5, 5, '5wibble') - assert_not type.changed?(5, 5, '5') - assert_not type.changed?(5, 5, '5.0') - assert_not type.changed?(nil, nil, nil) + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') end def test_type_cast_float type = Type::Float.new - assert_equal 1.0, type.type_cast_from_user("1") + assert_equal 1.0, type.cast("1") end def test_changing_float @@ -97,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 @@ -155,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/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 4a92da38ce..952e1681a7 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'models/owner' require 'models/pet' +require 'models/person' class LengthValidationTest < ActiveRecord::TestCase fixtures :owners @@ -36,7 +36,7 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association_utf8 repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + Owner.validates_size_of :pets, :minimum => 1 o = Owner.new('name' => 'あいうえおかきくけこ') assert !o.save assert o.errors[:pets].any? @@ -44,4 +44,34 @@ class LengthValidationTest < ActiveRecord::TestCase assert o.valid? end end + + def test_validates_size_of_respects_records_marked_for_destruction + Owner.validates_size_of :pets, minimum: 1 + owner = Owner.new + assert_not owner.save + assert owner.errors[:pets].any? + pet = owner.pets.build + assert owner.valid? + assert owner.save + + pet_count = Pet.count + assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ] + assert_not owner.valid? + assert owner.errors[:pets].any? + assert_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 18221cc73d..062bc733f9 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' @@ -30,18 +29,13 @@ class ReplyWithTitleObject < Reply def title; ReplyTitle.new; end end -class Employee < ActiveRecord::Base - self.table_name = 'postgresql_arrays' - validates_uniqueness_of :nicknames -end - class TopicWithUniqEvent < Topic belongs_to :event, foreign_key: :parent_id validates :event, uniqueness: true end class UniquenessValidationTest < ActiveRecord::TestCase - fixtures :topics, 'warehouse-things', :developers + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -378,18 +372,6 @@ class UniquenessValidationTest < ActiveRecord::TestCase } end - if current_adapter? :PostgreSQLAdapter - def test_validate_uniqueness_with_array_column - e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) - assert e1.persisted?, "Saving e1" - - e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) - assert !e2.persisted?, "e2 shouldn't be valid" - assert e2.errors[:nicknames].any?, "Should have errors for nicknames" - assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" - end - end - def test_validate_uniqueness_on_existing_relation event = Event.create assert TopicWithUniqEvent.create(event: event).valid? @@ -403,4 +385,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_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index 2bbf0f23b3..b30666d876 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -5,9 +5,7 @@ module ActiveRecord module ClassMethods def repair_validations(*model_classes) teardown do - model_classes.each do |k| - k.clear_validators! - end + model_classes.each(&:clear_validators!) end end end @@ -15,9 +13,7 @@ module ActiveRecord def repair_validations(*model_classes) yield if block_given? ensure - model_classes.each do |k| - k.clear_validators! - end + model_classes.each(&:clear_validators!) end end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 55804f9576..f4f316f393 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -1,9 +1,9 @@ -# encoding: utf-8 require "cases/helper" require 'models/topic' require 'models/reply' require 'models/person' require 'models/developer' +require 'models/computer' require 'models/parrot' require 'models/company' @@ -148,4 +148,17 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal 1, Company.validators_on(:name).size end + def test_numericality_validation_with_mutation + Topic.class_eval do + attribute :wibble, :string + validates_numericality_of :wibble, only_integer: true + end + + topic = Topic.new(wibble: '123-4567') + topic.wibble.gsub!('-', '') + + assert topic.valid? + ensure + Topic.reset_column_information + end 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/config.example.yml b/activerecord/test/config.example.yml index ce30cff9e7..e3b55d640e 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -69,12 +69,6 @@ connections: username: rails encoding: utf8 - openbase: - arunit: - username: admin - arunit2: - username: admin - oracle: arunit: adapter: oracle_enhanced diff --git a/activerecord/test/fixtures/bulbs.yml b/activerecord/test/fixtures/bulbs.yml new file mode 100644 index 0000000000..e5ce2b796c --- /dev/null +++ b/activerecord/test/fixtures/bulbs.yml @@ -0,0 +1,5 @@ +defaulty: + name: defaulty + +special: + name: special diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml index 7281a4d768..ad5ae2ec71 100644 --- a/activerecord/test/fixtures/computers.yml +++ b/activerecord/test/fixtures/computers.yml @@ -3,3 +3,8 @@ workstation: system: 'Linux' developer: 1 extendedWarranty: 1 + +laptop: + system: 'MacOS 1' + developer: 1 + extendedWarranty: 1 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/developers.yml b/activerecord/test/fixtures/developers.yml index 1a74563dc6..54b74e7a74 100644 --- a/activerecord/test/fixtures/developers.yml +++ b/activerecord/test/fixtures/developers.yml @@ -2,6 +2,7 @@ david: id: 1 name: David salary: 80000 + shared_computers: laptop jamis: id: 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/naked/csv/accounts.csv b/activerecord/test/fixtures/naked/csv/accounts.csv deleted file mode 100644 index 8b13789179..0000000000 --- a/activerecord/test/fixtures/naked/csv/accounts.csv +++ /dev/null @@ -1 +0,0 @@ - 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/author.rb b/activerecord/test/models/author.rb index 8949cf5826..8c1f14bd36 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,5 +1,6 @@ class Author < ActiveRecord::Base has_many :posts + has_many :serialized_posts has_one :post has_many :very_special_comments, :through => :posts has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post" @@ -44,13 +45,14 @@ class Author < ActiveRecord::Base has_many :special_posts has_many :special_post_comments, :through => :special_posts, :source => :comments + has_many :special_posts_with_default_scope, :class_name => 'SpecialPostWithDefaultScope' has_many :sti_posts, :class_name => 'StiPost' has_many :sti_post_comments, :through => :sti_posts, :source => :comments - has_many :special_nonexistant_posts, -> { where("posts.body = 'nonexistant'") }, :class_name => "SpecialPost" - has_many :special_nonexistant_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistant_posts, :source => :comments - has_many :nonexistant_comments, :through => :posts + has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, :class_name => "SpecialPost" + has_many :special_nonexistent_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistent_posts, :source => :comments + has_many :nonexistent_comments, :through => :posts has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post" has_many :hello_post_comments, :through => :hello_posts, :source => :comments 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/bird.rb b/activerecord/test/models/bird.rb index dff099c1fb..2a51d903b8 100644 --- a/activerecord/test/models/bird.rb +++ b/activerecord/test/models/bird.rb @@ -7,6 +7,6 @@ class Bird < ActiveRecord::Base attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method - false + throw(:abort) end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 831a0d5387..a6e83fe353 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -46,6 +46,6 @@ end class FailedBulb < Bulb before_destroy do - false + throw(:abort) end end 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/company.rb b/activerecord/test/models/company.rb index 42f7fb4680..6961f8fd6f 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -72,6 +72,7 @@ class Firm < Company # Oracle tests were failing because of that as the second fixture was selected has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account" has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account" + has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account" @@ -213,7 +214,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/customer.rb b/activerecord/test/models/customer.rb index 7e8e82542f..afe4b3d707 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -2,7 +2,7 @@ class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true - composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new(&:to_money) composed_of :gps_location, :allow_nil => true composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 3627cfdd09..d2a5a7fc49 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -15,6 +15,8 @@ class Developer < ActiveRecord::Base accepts_nested_attributes_for :projects + has_and_belongs_to_many :shared_computers, class_name: "Computer" + has_and_belongs_to_many :projects_extended_by_name, -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", 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/image.rb b/activerecord/test/models/image.rb new file mode 100644 index 0000000000..7ae8e4a7f6 --- /dev/null +++ b/activerecord/test/models/image.rb @@ -0,0 +1,3 @@ +class Image < ActiveRecord::Base + belongs_to :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 72095f9236..dc0566d8a7 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -27,6 +27,9 @@ class Member < ActiveRecord::Base has_many :clubs, :through => :current_memberships has_one :club_through_many, :through => :current_memberships, :source => :club + + belongs_to :admittable, polymorphic: true + has_one :premium_club, through: :admittable end class SelfMember < ActiveRecord::Base 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/organization.rb b/activerecord/test/models/organization.rb index 72e7bade68..f3e92f3067 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -8,5 +8,7 @@ class Organization < ActiveRecord::Base has_one :author, :primary_key => :name has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category + has_many :posts, :through => :author, :source => :posts + scope :clubs, -> { from('clubs') } end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 2e3a9a3681..cedb774b10 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -17,6 +17,8 @@ class Owner < ActiveRecord::Base after_commit :execute_blocks + accepts_nested_attributes_for :pets, allow_destroy: true + def blocks @blocks ||= [] end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index e76e83f314..b26035d944 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -11,7 +11,7 @@ class Parrot < ActiveRecord::Base attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method - false + throw(:abort) end end @@ -19,7 +19,7 @@ class LiveParrot < Parrot end class DeadParrot < Parrot - belongs_to :killer, :class_name => 'Pirate' + belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id end class FunkyParrot < Parrot diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 90a3c3ecee..30545bdcd7 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -36,8 +36,8 @@ class Pirate < ActiveRecord::Base has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb" - accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } - accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc(&:empty?) + accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) accepts_nested_attributes_for :update_only_ship, :update_only => true accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks, :birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true @@ -56,7 +56,7 @@ class Pirate < ActiveRecord::Base attr_accessor :cancel_save_from_callback, :parrots_limit before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method - false + throw(:abort) end private @@ -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 256b720c9a..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 @@ -72,14 +75,11 @@ class Post < ActiveRecord::Base through: :author_with_address, source: :author_address_extra - has_many :comments_with_interpolated_conditions, - ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' }, - :class_name => 'Comment' - has_one :very_special_comment has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment" + has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment" has_many :special_comments - has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' + has_many :nonexistent_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings @@ -127,6 +127,9 @@ class Post < ActiveRecord::Base has_many :taggings_using_author_id, :primary_key => :author_id, :as => :taggable, :class_name => 'Tagging' has_many :tags_using_author_id, :through => :taggings_using_author_id, :source => :tag + has_many :images, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class + has_one :main_image, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class, :class_name => 'Image' + has_many :standard_categorizations, :class_name => 'Categorization', :foreign_key => :post_id has_many :author_using_custom_pk, :through => :standard_categorizations has_many :authors_using_custom_pk, :through => :standard_categorizations @@ -219,8 +222,21 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base end end +class PostWithAfterCreateCallback < ActiveRecord::Base + self.table_name = 'posts' + has_many :comments, foreign_key: :post_id + + after_create do |post| + update_attribute(:author_id, comments.first.id) + end +end + class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base self.table_name = 'posts' has_many :comment_with_default_scope_references_associations, foreign_key: :post_id has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id end + +class SerializedPost < ActiveRecord::Base + serialize :title +end 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/ship.rb b/activerecord/test/models/ship.rb index 77a4728d0b..312caef604 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -4,9 +4,10 @@ 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 { |attributes| attributes.empty? } + accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) accepts_nested_attributes_for :update_only_pirate, :update_only => true validates_presence_of :name @@ -14,7 +15,7 @@ class Ship < ActiveRecord::Base attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method - false + throw(:abort) end end 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/tyre.rb b/activerecord/test/models/tyre.rb index bc3444aa7d..e50a21ca68 100644 --- a/activerecord/test/models/tyre.rb +++ b/activerecord/test/models/tyre.rb @@ -1,3 +1,11 @@ class Tyre < ActiveRecord::Base belongs_to :car + + def self.custom_find(id) + find(id) + end + + def self.custom_find_by(*args) + find_by(*args) + end end diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb new file mode 100644 index 0000000000..f5dc93e994 --- /dev/null +++ b/activerecord/test/models/user.rb @@ -0,0 +1,8 @@ +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/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index a7817772f4..264d9b8910 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -32,10 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000 fixed_time date default TO_DATE('2004-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'), char1 varchar2(1) default 'Y', char2 varchar2(50) default 'a varchar field', - char3 clob default 'a text field', - positive_integer integer default 1, - negative_integer integer default -1, - decimal_number number(3,2) default 2.78 + char3 clob default 'a text field' ) SQL execute "create sequence defaults_seq minvalue 10000" diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index e9294a11b9..f84be0e7f4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,8 +1,8 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones + postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| + drop_table table_name, if_exists: true end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -12,8 +12,6 @@ ActiveRecord::Schema.define do execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' - execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" - %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -30,101 +28,13 @@ ActiveRecord::Schema.define do char1 char(1) default 'Y', char2 character varying(50) default 'a varchar field', char3 text default 'a text field', - positive_integer integer default 1, - negative_integer integer default -1, bigint_default bigint default 0::bigint, - decimal_number decimal(3,2) default 2.78, multiline_default text DEFAULT '--- [] '::text ); _SQL - execute "CREATE SCHEMA schema_1" - execute "CREATE DOMAIN schema_1.text AS text" - execute "CREATE DOMAIN schema_1.varchar AS varchar" - execute "CREATE DOMAIN schema_1.bpchar AS bpchar" - - execute <<_SQL - CREATE TABLE geometrics ( - id serial primary key, - a_point point, - -- a_line line, (the line type is currently not implemented in postgresql) - a_line_segment lseg, - a_box box, - a_path path, - a_polygon polygon, - a_circle circle - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_arrays ( - id SERIAL PRIMARY KEY, - commission_by_quarter INTEGER[], - nicknames TEXT[] - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_uuids ( - id SERIAL PRIMARY KEY, - guid uuid, - compact_guid uuid - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_tsvectors ( - id SERIAL PRIMARY KEY, - text_vector tsvector - ); -_SQL - - if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_hstores ( - id SERIAL PRIMARY KEY, - hash_store hstore default ''::hstore - ); -_SQL - end - - if 't' == select_value("select 'ltree'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_ltrees ( - id SERIAL PRIMARY KEY, - path ltree - ); -_SQL - end - - if 't' == select_value("select 'citext'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_citext ( - id SERIAL PRIMARY KEY, - text_citext citext default ''::citext - ); -_SQL - end - - if 't' == select_value("select 'json'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_json_data_type ( - id SERIAL PRIMARY KEY, - json_data json default '{}'::json - ); -_SQL - end - - execute <<_SQL - CREATE TABLE postgresql_numbers ( - id SERIAL PRIMARY KEY, - single REAL, - double DOUBLE PRECISION - ); -_SQL - execute <<_SQL CREATE TABLE postgresql_times ( id SERIAL PRIMARY KEY, @@ -134,15 +44,6 @@ _SQL _SQL execute <<_SQL - CREATE TABLE postgresql_network_addresses ( - id SERIAL PRIMARY KEY, - cidr_address CIDR default '192.168.1.0/24', - inet_address INET default '192.168.1.1', - mac_address MACADDR default 'ff:ff:ff:ff:ff:ff' - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_oids ( id SERIAL PRIMARY KEY, obj_id OID @@ -187,16 +88,6 @@ _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 diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 10de3d34cb..5e5f7a798e 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) @@ -228,6 +227,11 @@ ActiveRecord::Schema.define do t.integer :extendedWarranty, null: false end + create_table :computers_developers, id: false, force: true do |t| + t.references :computer + t.references :developer + end + create_table :contracts, force: true do |t| t.integer :developer_id t.integer :company_id @@ -311,7 +315,7 @@ ActiveRecord::Schema.define do end create_table :cold_jokes, force: true do |t| - t.string :name + t.string :cold_name end create_table :friendships, force: true do |t| @@ -464,6 +468,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 @@ -474,6 +482,8 @@ ActiveRecord::Schema.define do # Oracle/SQLServer supports precision up to 38 if current_adapter?(:OracleAdapter, :SQLServerAdapter) t.decimal :atoms_in_universe, precision: 38, scale: 0 + elsif current_adapter?(:FbAdapter) + t.decimal :atoms_in_universe, precision: 18, scale: 0 else t.decimal :atoms_in_universe, precision: 55, scale: 0 end @@ -585,6 +595,16 @@ ActiveRecord::Schema.define do t.integer :tags_with_nullify_count, default: 0 end + create_table :serialized_posts, force: true do |t| + t.integer :author_id + t.string :title, null: false + end + + create_table :images, force: true do |t| + t.integer :imageable_identifier + t.string :imageable_class + end + create_table :price_estimates, force: true do |t| t.string :estimate_of_type t.integer :estimate_of_id @@ -606,7 +626,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 @@ -650,6 +680,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| @@ -711,7 +742,7 @@ ActiveRecord::Schema.define do t.string :author_name t.string :author_email_address if mysql_56? - t.datetime :written_on, limit: 6 + t.datetime :written_on, precision: 6 else t.datetime :written_on end @@ -755,6 +786,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| @@ -877,6 +909,11 @@ ActiveRecord::Schema.define do t.string :overloaded_string_with_limit, limit: 255 t.string :string_with_default, default: 'the original default' end + + create_table :users, force: true do |t| + t.string :token + t.string :auth_token + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 8b76fa97d1..35be823ea1 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,343 +1,205 @@ -* `MessageVerifier.new` raises an appropriate exception if the secret is `nil`. - This prevents `MessageVerifier#generate` from raising a cryptic error later on. +* Fixed a problem where String#truncate_words would get stuck with a complex + string. - *Kostiantyn Kahanskyi* + *Henrik Nygren* -* Introduced new configuration option `active_support.test_order` for - specifying the order test cases are executed. This option currently defaults - to `:sorted` but will be changed to `:random` in Rails 5.0. - - *Akira Matsuda*, *Godfrey Chan* - -* Fixed a bug in Inflector#underscore where acroynms in nested constant names - are incorrectly parsed as camelCase. - - Fixes #8015. - - *Fred Wu*, *Matthew Draper* - -* Make Time#change throw an exception if the :usec option is out of range and - the time has an offset other than UTC or local. - - *Agis Anastasopoulos* - -* `Method` objects now report themselves as not `duplicable?`. This allows - hashes and arrays containing `Method` objects to be `deep_dup`ed. - - *Peter Jaros* - -* `determine_constant_from_test_name` does no longer shadow `NameError`s - which happen during constant autoloading. - - Fixes #9933. - - *Guo Xiang Tan* - -* Added instance_eval version to Object#try, so you can do this: - - person.try { name.first } - - instead of: - - person.try { |person| person.name.first } - - *DHH* - -* Fix the `ActiveSupport::Duration#instance_of?` method to return the right - value with the class itself since it was previously delegated to the - internal value. - - *Robin Dupret* - -* Fix rounding errors with #travel_to by resetting the usec on any passed time to zero, so we only travel - with per-second precision, not anything deeper than that. - - *DHH* - -* Fix DateTime comparison with DateTime::Infinity object. - - *Rafael Mendonça França* - -* Added Object#itself which returns the object itself. Useful when dealing with a chaining scenario, like Active Record scopes: - - Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at) - - *DHH* - -* `Object#with_options` executes block in merging option context when - explicit receiver in not passed. - - *Pavel Pravosud* - -* Fixed a compatibility issue with the `Oj` gem when cherry-picking the file - `active_support/core_ext/object/json` without requiring `active_support/json`. - - Fixes #16131. - - *Godfrey Chan* - -* Make `Hash#with_indifferent_access` copy the default proc too. - - *arthurnn*, *Xanders* - -* Add `String#truncate_words` to truncate a string by a number of words. - - *Mohamed Osama* - -* Deprecate `capture` and `quietly`. - - These methods are not thread safe and may cause issues when used in threaded environments. - To avoid problems we are deprecating them. - - *Tom Meier* - -* `DateTime#to_f` now preserves the fractional seconds instead of always - rounding to `.0`. - - Fixes #15994. - - *John Paul Ashenfelter* - -* Add `Hash#transform_values` to simplify a common pattern where the values of a - hash must change, but the keys are left the same. - - *Sean Griffin* - -* Always instrument `ActiveSupport::Cache`. - - Since `ActiveSupport::Notifications` only instrument items when there - are subscriber we don't need to disable instrumentation. - - *Peter Wagenet* - -* Make the `apply_inflections` method case-insensitive when checking - whether a word is uncountable or not. - - *Robin Dupret* - -* Make Dependencies pass a name to NameError error. - - *arthurnn* - -* Fixed `ActiveSupport::Cache::FileStore` exploding with long paths. - - *Adam Panzer / Michael Grosser* - -* Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost - when working with objects with a nanosecond component. - - `ActiveSupport::TimeWithZone#-` should return the same result as if we were - using `Time#-`: - - Time.now.end_of_day - Time.now.beginning_of_day #=> 86399.999999999 +* Fixed a roundtrip problem with AS::SafeBuffer where primitive-like strings + will be dumped as primitives: Before: - Time.zone.now.end_of_day.nsec #=> 999999999 - Time.zone.now.end_of_day - Time.zone.now.beginning_of_day #=> 86400.0 + 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: - Time.zone.now.end_of_day - Time.zone.now.beginning_of_day - #=> 86399.999999999 - - *Gordon Chan* - -* Fixed precision error in NumberHelper when using Rationals. - - Before: - - ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 - #=> "330.00" - - After: - - ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 - #=> "333.33" - - See #15379. - - *Juanjo Bazán* - -* Removed deprecated `Numeric#ago` and friends - - Replacements: - - 5.ago => 5.seconds.ago - 5.until => 5.seconds.until - 5.since => 5.seconds.since - 5.from_now => 5.seconds.from_now - - See #12389 for the history and rationale behind this. + 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* -* DateTime `advance` now supports partial days. +* Enable `number_to_percentage` to keep the number's precision by allowing + `:precision` to be `nil`. - Before: + *Jack Xu* - DateTime.now.advance(days: 1, hours: 12) +* `config_accessor` became a private method, as with Ruby's `attr_accessor`. - After: + *Akira Matsuda* - DateTime.now.advance(days: 1.5) +* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as + `Time.now` and `Date.today`. - Fixes #12005. + *Yuki Nishijima* - *Shay Davidson* +* Add `file_fixture` to `ActiveSupport::TestCase`. + It provides a simple mechanism to access sample files in your test cases. -* `Hash#deep_transform_keys` and `Hash#deep_transform_keys!` now transform hashes - in nested arrays. This change also applies to `Hash#deep_stringify_keys`, - `Hash#deep_stringify_keys!`, `Hash#deep_symbolize_keys` and - `Hash#deep_symbolize_keys!`. + By default file fixtures are stored in `test/fixtures/files`. This can be + configured per test-case using the `file_fixture_path` class attribute. - *OZAWA Sakuro* + *Yves Senn* -* Fixed confusing `DelegationError` in `Module#delegate`. +* Return value of yielded block in `File.atomic_write`. - See #15186. + *Ian Ker-Seymer* - *Vladimir Yarotsky* +* 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`. -* Fixed `ActiveSupport::Subscriber` so that no duplicate subscriber is created - when a subscriber method is redefined. + Fixes #18550. - *Dennis Schön* + *Aditya Kapoor* -* Remove deprecated string based terminators for `ActiveSupport::Callbacks`. +* Add missing time zone definitions for Russian Federation and sync them + with `zone.tab` file from tzdata version 2014j (latest). - *Eileen M. Uchitelle* + *Andrey Novikov* -* Fixed an issue when using - `ActiveSupport::NumberHelper::NumberToDelimitedConverter` to - convert a value that is an `ActiveSupport::SafeBuffer` introduced - in 2da9d67. +* Add `SecureRandom.base58` for generation of random base58 strings. - See #15064. + *Matthew Draper*, *Guillermo Iguaran* - *Mark J. Titorenko* +* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and + `#tomorrow` for `Date`, `Time`, and `DateTime`. -* `TimeZone#parse` defaults the day of the month to '1' if any other date - components are specified. This is more consistent with the behavior of - `Time#parse`. + *George Claghorn* - *Ulysse Carion* +* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, + and `DateTime`. -* `humanize` strips leading underscores, if any. + *George Claghorn* - Before: +* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, + `Time`, and `DateTime`. - '_id'.humanize # => "" + `#on_weekend?` returns true if the receiving date/time falls on a Saturday + or Sunday. - After: + `#next_weekday` returns a new date/time representing the next day that does + not fall on a Saturday or Sunday. - '_id'.humanize # => "Id" + `#prev_weekday` returns a new date/time representing the previous day that + does not fall on a Saturday or Sunday. - *Xavier Noria* + *George Claghorn* -* Fixed backward compatibility isues introduced in 326e652. +* Change the default test order from `:sorted` to `:random`. - Empty Hash or Array should not present in serialization result. + *Rafael Mendonça França* - {a: []}.to_query # => "" - {a: {}}.to_query # => "" +* Remove deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. + + *Rafael Mendonça França* - For more info see #14948. +* Remove deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=` + and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`. - *Bogdan Gusiev* + *Rafael Mendonça França* -* Add `Digest::UUID::uuid_v3` and `Digest::UUID::uuid_v5` to support stable - UUID fixtures on PostgreSQL. +* Remove deprecated `ActiveSupport::SafeBuffer#prepend`. - *Roderick van Domburg* + *Rafael Mendonça França* -* Fixed `ActiveSupport::Duration#eql?` so that `1.second.eql?(1.second)` is - true. +* Remove deprecated methods at `Kernel`. - This fixes the current situation of: + `silence_stderr`, `silence_stream`, `capture` and `quietly`. - 1.second.eql?(1.second) #=> false + *Rafael Mendonça França* - `eql?` also requires that the other object is an `ActiveSupport::Duration`. - This requirement makes `ActiveSupport::Duration`'s behavior consistent with - the behavior of Ruby's numeric types: +* Remove deprecated `active_support/core_ext/big_decimal/yaml_conversions` + file. - 1.eql?(1.0) #=> false - 1.0.eql?(1) #=> false + *Rafael Mendonça França* - 1.second.eql?(1) #=> false (was true) - 1.eql?(1.second) #=> false +* Remove deprecated methods `ActiveSupport::Cache::Store.instrument` and + `ActiveSupport::Cache::Store.instrument=`. - { 1 => "foo", 1.0 => "bar" } - #=> { 1 => "foo", 1.0 => "bar" } + *Rafael Mendonça França* - { 1 => "foo", 1.second => "bar" } - # now => { 1 => "foo", 1.second => "bar" } - # was => { 1 => "bar" } +* Change the way in which callback chains can be halted. - And though the behavior of these hasn't changed, for reference: + The preferred method to halt a callback chain from now on is to explicitly + `throw(:abort)`. + In the past, returning `false` in an ActiveSupport callback had the side + effect of halting the callback chain. This is not recommended anymore and, + depending on the value of + `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will + either not work at all or display a deprecation warning. - 1 == 1.0 #=> true - 1.0 == 1 #=> true +* Add Callbacks::CallbackChain.halt_and_display_warning_on_return_false - 1 == 1.second #=> true - 1.second == 1 #=> true + Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` + to true will let an app support the deprecated way of halting callback + chains by returning `false`. - *Emily Dobervich* + 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)`. -* `ActiveSupport::SafeBuffer#prepend` acts like `String#prepend` and modifies - instance in-place, returning self. `ActiveSupport::SafeBuffer#prepend!` is - deprecated. + The value can also be set with the Rails configuration option + `config.active_support.halt_callback_chains_on_return_false`. - *Pavel Pravosud* + When the configuration option is missing, its value is `true`, so older apps + ported to Rails 5.0 will not break (but display a deprecation warning). + For new Rails 5.0 apps, its value is set to `false` in an initializer, so + these apps will support the new behavior by default. -* `HashWithIndifferentAccess` better respects `#to_hash` on objects it's - given. In particular, `.new`, `#update`, `#merge`, `#replace` all accept - objects which respond to `#to_hash`, even if those objects are not Hashes - directly. + *claudiob* - *Peter Jaros* +* Changes arguments and default value of CallbackChain's :terminator option -* Deprecate `Class#superclass_delegating_accessor`, use `Class#class_attribute` instead. + Chains of callbacks defined without an explicit `:terminator` option will + now be halted as soon as a `before_` callback throws `:abort`. - *Akshay Vishnoi* + Chains of callbacks defined with a `:terminator` option will maintain their + existing behavior of halting as soon as a `before_` callback matches the + terminator's expectation. -* Ensure classes which `include Enumerable` get `#to_json` in addition to - `#as_json`. + *claudiob* - *Sammy Larbi* +* Deprecate `MissingSourceFile` in favor of `LoadError`. -* Change the signature of `fetch_multi` to return a hash rather than an - array. This makes it consistent with the output of `read_multi`. + `MissingSourceFile` was just an alias to `LoadError` and was not being + raised inside the framework. - *Parker Selbert* + *Rafael Mendonça França* -* Introduce `Concern#class_methods` as a sleek alternative to clunky - `module ClassMethods`. Add `Kernel#concern` to define at the toplevel - without chunky `module Foo; extend ActiveSupport::Concern` boilerplate. +* Add support for error dispatcher classes in `ActiveSupport::Rescuable`. + Now it acts closer to Ruby's rescue. - # app/models/concerns/authentication.rb - concern :Authentication do - included do - after_create :generate_private_key - end + Example: - class_methods do - def authenticate(credentials) - # ... + class BaseController < ApplicationController + module ErrorDispatcher + def self.===(other) + Exception === other && other.respond_to?(:status) end end - def generate_private_key - # ... + rescue_from ErrorDispatcher do |error| + render status: error.status, json: { error: error.to_s } end end - # app/models/user.rb - class User < ActiveRecord::Base - include Authentication - end + *Genadi Samokovarov* + +* Add `#verified` and `#valid_message?` methods to `ActiveSupport::MessageVerifier` + + Previously, the only way to decode a message with `ActiveSupport::MessageVerifier` + was to use `#verify`, which would raise an exception on invalid messages. Now + `#verified` can also be used, which returns `nil` on messages that cannot be + decoded. + + Previously, there was no way to check if a message's format was valid without + attempting to decode it. `#valid_message?` is a boolean convenience method that + checks whether the message is valid without actually decoding it. - *Jeremy Kemper* + *Logan Leger* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index d06d4f3b2d..7bffebb076 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2014 David Heinemeier Hansson +Copyright (c) 2005-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 5ba153662a..7c40df8dc8 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -7,9 +7,9 @@ Rake::TestTask.new do |t| t.pattern = 'test/**/*_test.rb' t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - namespace :test do task :isolated do Dir.glob("test/**/*_test.rb").all? do |file| diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 80d1d35888..a5339e6475 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -20,9 +20,9 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '>= 0.7.0.beta1', '< 0.8' + s.add_dependency 'i18n', '~> 0.7' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' - s.add_dependency 'thread_safe','~> 0.1' + s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4' end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index f39e89b7d0..71a6b78652 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -55,7 +55,7 @@ module ActiveSupport codepoint.combining_class = Integer($4) #codepoint.bidi_class = $5 codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex } + codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect(&:hex) #codepoint.bidi_mirrored = ($13=='Y') ? true : false codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 94468240a4..290920dbf8 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2014 David Heinemeier Hansson +# Copyright (c) 2005-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -71,15 +71,7 @@ module ActiveSupport NumberHelper.eager_load! end - @@test_order = nil - - def self.test_order=(new_order) - @@test_order = new_order - end - - def self.test_order - @@test_order - end + cattr_accessor :test_order # :nodoc: end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index ff67a6828c..fdef9cbfc0 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,7 +8,6 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' -require 'active_support/deprecation' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -179,18 +178,6 @@ module ActiveSupport @silence = previous_silence end - # :deprecated: - def self.instrument=(boolean) - ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." - true - end - - # :deprecated: - def self.instrument - ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." - true - end - # Fetches data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. # @@ -338,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 @@ -540,16 +530,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) @@ -615,14 +616,12 @@ module ActiveSupport end def value - convert_version_4beta1_entry! if defined?(@v) compressed? ? uncompress(@value) : @value end # Check if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? - convert_version_4beta1_entry! if defined?(@value) @expires_in && @created_at + @expires_in <= Time.now.to_f end @@ -658,8 +657,6 @@ module ActiveSupport # Duplicate the value in a class. This is used by cache implementations that don't natively # serialize entries to protect against accidental cache modifications. def dup_value! - convert_version_4beta1_entry! if defined?(@v) - if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) if @value.is_a?(String) @value = @value.dup @@ -692,26 +689,6 @@ module ActiveSupport def uncompress(value) Marshal.load(Zlib::Inflate.inflate(value)) end - - # The internals of this method changed between Rails 3.x and 4.0. This method provides the glue - # to ensure that cache entries created under the old version still work with the new class definition. - def convert_version_4beta1_entry! - if defined?(@v) - @value = @v - remove_instance_variable(:@v) - end - - if defined?(@c) - @compressed = @c - remove_instance_variable(:@c) - end - - if defined?(@x) && @x - @created_at ||= Time.now.to_f - @expires_in = @x - @created_at - remove_instance_variable(:@x) - end - end end end end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 61b4f0b8b0..73ae3acea5 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -66,14 +66,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/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index cd467e13f6..f32bb8a0cc 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/string/filters' require 'thread' module ActiveSupport @@ -78,18 +79,21 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - cbs = send("_#{kind}_callbacks") - if cbs.empty? - yield if block_given? + send "_run_#{kind}_callbacks", &block + end + + private + + def _run_callbacks(callbacks, &block) + if callbacks.empty? + block.call if block else - runner = cbs.compile + runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) runner.call(e).value 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. @@ -118,102 +122,106 @@ module ActiveSupport ENDING = End.new class Before - def self.build(next_callback, user_callback, user_conditions, chain_config, filter) + def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter) halted_lambda = chain_config[:terminator] if chain_config.key?(:terminator) && user_conditions.any? - halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) + halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) elsif chain_config.key? :terminator - halting(next_callback, user_callback, halted_lambda, filter) + halting(callback_sequence, user_callback, halted_lambda, filter) elsif user_conditions.any? - conditional(next_callback, user_callback, user_conditions) + conditional(callback_sequence, user_callback, user_conditions) else - simple next_callback, user_callback + simple callback_sequence, user_callback end end - def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) - lambda { |env| + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) + callback_sequence.before do |env| target = env.target value = env.value halted = env.halted if !halted && user_conditions.all? { |c| c.call(target, value) } - result = user_callback.call target, value - env.halted = halted_lambda.call(target, result) + result_lambda = -> { user_callback.call target, value } + env.halted = halted_lambda.call(target, result_lambda) if env.halted target.send :halted_callback_hook, filter end end - next_callback.call env - } + + env + end end private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback, halted_lambda, filter) - lambda { |env| + def self.halting(callback_sequence, user_callback, halted_lambda, filter) + callback_sequence.before do |env| target = env.target value = env.value halted = env.halted unless halted - result = user_callback.call target, value - env.halted = halted_lambda.call(target, result) + result_lambda = -> { user_callback.call target, value } + env.halted = halted_lambda.call(target, result_lambda) + if env.halted target.send :halted_callback_hook, filter end end - next_callback.call env - } + + env + end end private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.before do |env| target = env.target value = env.value if user_conditions.all? { |c| c.call(target, value) } user_callback.call target, value end - next_callback.call env - } + + env + end end private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| + def self.simple(callback_sequence, user_callback) + callback_sequence.before do |env| user_callback.call env.target, env.value - next_callback.call env - } + + env + end end private_class_method :simple end class After - def self.build(next_callback, user_callback, user_conditions, chain_config) + def self.build(callback_sequence, user_callback, user_conditions, chain_config) if chain_config[:skip_after_callbacks_if_terminated] if chain_config.key?(:terminator) && user_conditions.any? - halting_and_conditional(next_callback, user_callback, user_conditions) + halting_and_conditional(callback_sequence, user_callback, user_conditions) elsif chain_config.key?(:terminator) - halting(next_callback, user_callback) + halting(callback_sequence, user_callback) elsif user_conditions.any? - conditional next_callback, user_callback, user_conditions + conditional callback_sequence, user_callback, user_conditions else - simple next_callback, user_callback + simple callback_sequence, user_callback end else if user_conditions.any? - conditional next_callback, user_callback, user_conditions + conditional callback_sequence, user_callback, user_conditions else - simple next_callback, user_callback + simple callback_sequence, user_callback end end end - def self.halting_and_conditional(next_callback, user_callback, user_conditions) - lambda { |env| - env = next_callback.call env + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| target = env.target value = env.value halted = env.halted @@ -221,122 +229,119 @@ module ActiveSupport if !halted && user_conditions.all? { |c| c.call(target, value) } user_callback.call target, value end + env - } + end end private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback) - lambda { |env| - env = next_callback.call env + def self.halting(callback_sequence, user_callback) + callback_sequence.after do |env| unless env.halted user_callback.call env.target, env.value end + env - } + end end private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| - env = next_callback.call env + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| target = env.target value = env.value if user_conditions.all? { |c| c.call(target, value) } user_callback.call target, value end + env - } + end end private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| - env = next_callback.call env + def self.simple(callback_sequence, user_callback) + callback_sequence.after do |env| user_callback.call env.target, env.value + env - } + end end private_class_method :simple end class Around - def self.build(next_callback, user_callback, user_conditions, chain_config) + def self.build(callback_sequence, user_callback, user_conditions, chain_config) if chain_config.key?(:terminator) && user_conditions.any? - halting_and_conditional(next_callback, user_callback, user_conditions) + halting_and_conditional(callback_sequence, user_callback, user_conditions) elsif chain_config.key? :terminator - halting(next_callback, user_callback) + halting(callback_sequence, user_callback) elsif user_conditions.any? - conditional(next_callback, user_callback, user_conditions) + conditional(callback_sequence, user_callback, user_conditions) else - simple(next_callback, user_callback) + simple(callback_sequence, user_callback) end end - def self.halting_and_conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.around do |env, &run| target = env.target value = env.value halted = env.halted if !halted && user_conditions.all? { |c| c.call(target, value) } user_callback.call(target, value) { - env = next_callback.call env - env.value + run.call.value } env else - next_callback.call env + run.call end - } + end end private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback) - lambda { |env| + def self.halting(callback_sequence, user_callback) + callback_sequence.around do |env, &run| target = env.target value = env.value if env.halted - next_callback.call env + run.call else user_callback.call(target, value) { - env = next_callback.call env - env.value + run.call.value } env end - } + end end private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.around do |env, &run| target = env.target value = env.value if user_conditions.all? { |c| c.call(target, value) } user_callback.call(target, value) { - env = next_callback.call env - env.value + run.call.value } env else - next_callback.call env + run.call end - } + end end private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| + def self.simple(callback_sequence, user_callback) + callback_sequence.around do |env, &run| user_callback.call(env.target, env.value) { - env = next_callback.call env - env.value + run.call.value } env - } + end end private_class_method :simple end @@ -363,14 +368,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 @@ -389,17 +394,17 @@ module ActiveSupport end # Wraps code with filter - def apply(next_callback) + def apply(callback_sequence) user_conditions = conditions_lambdas user_callback = make_lambda @filter case kind when :before - Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter) + Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter) when :after - Filters::After.build(next_callback, user_callback, user_conditions, chain_config) + Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config) when :around - Filters::Around.build(next_callback, user_callback, user_conditions, chain_config) + Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config) end end @@ -464,16 +469,59 @@ module ActiveSupport end end + # Execute before and after filters in a sequence instead of + # chaining them with nested lambda calls, see: + # https://github.com/rails/rails/issues/18011 + class CallbackSequence + def initialize(&call) + @call = call + @before = [] + @after = [] + end + + def before(&before) + @before.unshift(before) + self + end + + def after(&after) + @after.push(after) + self + end + + def around(&around) + CallbackSequence.new do |*args| + around.call(*args) { + self.call(*args) + } + end + end + + def call(*args) + @before.each { |b| b.call(*args) } + value = @call.call(*args) + @after.each { |a| a.call(*args) } + value + end + end + # An Array with a compile method. class CallbackChain #:nodoc:# include Enumerable attr_reader :name, :config + # If true, any callback returning +false+ will halt the entire callback + # chain and display a deprecation message. If false, callback chains will + # only be halted by calling +throw :abort+. Defaults to +true+. + class_attribute :halt_and_display_warning_on_return_false + self.halt_and_display_warning_on_return_false = true + def initialize(name, config) @name = name @config = { - :scope => [ :kind ] + scope: [:kind], + terminator: default_terminator }.merge!(config) @chain = [] @callbacks = nil @@ -508,8 +556,9 @@ module ActiveSupport def compile @callbacks || @mutex.synchronize do - @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback| - callback.apply chain + final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } + @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| + callback.apply callback_sequence end end end @@ -543,6 +592,28 @@ module ActiveSupport @callbacks = nil @chain.delete_if { |c| callback.duplicates?(c) } end + + def default_terminator + Proc.new do |target, result_lambda| + terminate = true + catch(:abort) do + result = result_lambda.call if result_lambda.is_a?(Proc) + if halt_and_display_warning_on_return_false && result == false + display_deprecation_warning_for_false_terminator + else + terminate = false + end + end + terminate + end + end + + def display_deprecation_warning_for_false_terminator + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails. + To explicitly halt a callback chain, please use `throw :abort` instead. + MSG + end end module ClassMethods @@ -556,7 +627,7 @@ module ActiveSupport # This is used internally to append, prepend and skip callbacks to the # CallbackChain. def __update_callbacks(name) #:nodoc: - ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target| + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target| chain = target.get_callbacks name yield target, chain.dup end @@ -625,7 +696,7 @@ module ActiveSupport filter = chain.find {|c| c.matches?(type, filter) } if filter && options.any? - new_filter = filter.merge(chain, options) + new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) chain.insert(chain.index(filter), new_filter) end @@ -656,21 +727,22 @@ module ActiveSupport # ===== Options # # * <tt>:terminator</tt> - Determines when a before filter will halt the - # callback chain, preventing following callbacks from being called and - # the event from being triggered. This should be a lambda to be executed. + # callback chain, preventing following before and around callbacks from + # being called and the event from being triggered. + # This should be a lambda to be executed. # The current object and the return result of the callback will be called # with the lambda. # # define_callbacks :validate, terminator: ->(target, result) { result == false } # # In this example, if any before validate callbacks returns +false+, - # other callbacks are not executed. Defaults to +false+, meaning no value - # halts the chain. + # any successive before and around callback is not executed. + # Defaults to +false+, meaning no value halts the chain. # # * <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 @@ -716,12 +788,21 @@ module ActiveSupport # define_callbacks :save, scope: [:name] # # would call <tt>Audit#save</tt>. + # + # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # `!`, `?` or `=`. def define_callbacks(*names) options = names.extract_options! 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 9d5cee54e3..4082d2d464 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -95,7 +95,7 @@ module ActiveSupport # end # # class Host - # include Bar # works, Bar takes care now of its dependencies + # include Bar # It works, now Bar takes care of its dependencies # end module Concern class MultipleIncludedBlocks < StandardError #:nodoc: @@ -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) 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.rb b/activesupport/lib/active_support/core_ext.rb index 199aa91020..52706c3d7a 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,3 +1,4 @@ -Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path| +DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"] +(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path| require path end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index caa499dfa2..ca66d806ef 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -20,7 +20,11 @@ class Array # %w( a b c d ).to(-2) # => ["a", "b", "c"] # %w( a b c ).to(-10) # => [] def to(position) - self[0..position] + if position >= 0 + take position + 1 + else + self[0..position] + end end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 76ffd23ed1..080e3b5ef7 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -92,7 +92,7 @@ class Array if empty? 'null' else - collect { |element| element.id }.join(',') + collect(&:id).join(',') end else to_default_s diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 843c592669..234283e792 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -3,14 +3,13 @@ require 'bigdecimal/util' class BigDecimal DEFAULT_STRING_FORMAT = 'F' - def to_formatted_s(*args) - if args[0].is_a?(Symbol) - super + alias_method :to_default_s, :to_s + + def to_s(format = nil, options = nil) + if format.is_a?(Symbol) + to_formatted_s(format, options || {}) else - format = args[0] || DEFAULT_STRING_FORMAT - _original_to_s(format) + to_default_s(format || DEFAULT_STRING_FORMAT) end end - alias_method :_original_to_s, :to_s - alias_method :to_s, :to_formatted_s end diff --git a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb deleted file mode 100644 index 46ba93ead4..0000000000 --- a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb +++ /dev/null @@ -1,14 +0,0 @@ -ActiveSupport::Deprecation.warn 'core_ext/big_decimal/yaml_conversions is deprecated and will be removed in the future.' - -require 'bigdecimal' -require 'yaml' -require 'active_support/core_ext/big_decimal/conversions' - -class BigDecimal - YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } - - def encode_with(coder) - string = to_s - coder.represent_scalar(nil, YAML_MAPPING[string] || string) - end -end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index f2a221c396..f2b7bb3ef1 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -116,12 +116,4 @@ class Class attr_writer name if instance_writer end end - - private - - unless respond_to?(:singleton_class?) - def singleton_class? - ancestors.first != self - end - end end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index b85e49aca5..9525c10112 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) @@ -111,9 +127,19 @@ module DateAndTime # Returns a new date/time representing the given day in the next week. # 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. +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + 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 +160,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 +272,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/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index dc4e767e9d..55ad384f4f 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -10,7 +10,11 @@ class DateTime end end - # Seconds since midnight: DateTime.now.seconds_since_midnight. + # Returns the number of seconds since 00:00:00. + # + # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 + # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 + # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 def seconds_since_midnight sec + (min * 60) + (hour * 3600) end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 0e7e3ba378..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,7 +40,10 @@ class File chown(old_stat.uid, old_stat.gid, file_name) # This operation will affect filesystem ACL's chmod(old_stat.mode, file_name) - rescue Errno::EPERM + + # Make sure we return the result of the yielded block + return_val + rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end end diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb index 6566215a4d..5dc9a05ec7 100644 --- a/activesupport/lib/active_support/core_ext/hash/compact.rb +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -1,6 +1,6 @@ class Hash # Returns a hash with non +nil+ values. - # + # # hash = { a: true, b: false, c: nil} # hash.compact # => { a: true, b: false} # hash # => { a: true, b: false, c: nil} @@ -8,9 +8,9 @@ class Hash def compact self.select { |_, value| !value.nil? } end - + # Replaces current hash with non +nil+ values. - # + # # hash = { a: true, b: false, c: nil} # hash.compact! # => { a: true, b: false} # hash # => { a: true, b: false} diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index 763d563231..9c9faf67ea 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -4,7 +4,7 @@ class Hash # h1 = { a: true, b: { c: [1, 2, 3] } } # h2 = { a: false, b: { x: [3, 4, 5] } } # - # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } + # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } # # Like with Hash#merge in the standard library, a block can be provided # to merge values: diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 682d089881..6e397abf51 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,13 +1,19 @@ class Hash - # Returns a hash that includes everything but the given keys. This is useful for - # limiting a set of parameters to everything but a few known toggles: + # Returns a hash that includes everything but the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except(:c) # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} # + # This is useful for limiting a set of parameters to everything but a few known toggles: # @person.update(params[:person].except(:admin)) def except(*keys) dup.except!(*keys) end # Replaces the hash without the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except!(:c) # => { a: true, b: false} + # hash # => { a: true, b: false } def except!(*keys) keys.each { |key| delete(key) } self diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index f4105f66b0..9297a59c46 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -31,13 +31,13 @@ class Hash # hash.stringify_keys # # => {"name"=>"Rob", "age"=>"28"} def stringify_keys - transform_keys{ |key| key.to_s } + transform_keys(&:to_s) end # Destructively convert all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! - transform_keys!{ |key| key.to_s } + transform_keys!(&:to_s) end # Returns a new hash with all keys converted to symbols, as long as @@ -105,14 +105,14 @@ class Hash # hash.deep_stringify_keys # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} def deep_stringify_keys - deep_transform_keys{ |key| key.to_s } + deep_transform_keys(&:to_s) end # Destructively convert all keys to strings. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_stringify_keys! - deep_transform_keys!{ |key| key.to_s } + deep_transform_keys!(&:to_s) end # Returns a new hash with all keys converted to symbols, as long as diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 8ad600b171..41b2279013 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,6 +1,12 @@ class Hash - # Slice a hash to include only the given keys. This is useful for - # limiting an options hash to valid keys before passing to a method: + # Slice 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) + # # => {:a=>1, :b=>2} + # + # This is useful for limiting an options hash to valid keys before + # passing to a method: # # def search(criteria = {}) # criteria.assert_valid_keys(:mass, :velocity, :time) diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 293a3b2619..364ed9d65f 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/kernel/agnostics' require 'active_support/core_ext/kernel/concern' -require 'active_support/core_ext/kernel/debugger' if RUBY_VERSION < '2.0.0' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb index 2073cac98d..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb +++ b/activesupport/lib/active_support/core_ext/kernel/debugger.rb @@ -1,10 +1,3 @@ -module Kernel - unless respond_to?(:debugger) - # Starts a debugging session if the +debugger+ gem has been loaded (call rails server --debugger to do load it). - def debugger - message = "\n***** Debugger requested, but was not available (ensure the debugger gem is listed in Gemfile/installed as gem): Start server with --debugger to enable *****\n" - defined?(Rails) ? Rails.logger.info(message) : $stderr.puts(message) - end - alias breakpoint debugger unless respond_to?(:breakpoint) - end -end +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 80c531b694..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 @@ -29,34 +28,6 @@ module Kernel $VERBOSE = old_verbose end - # For compatibility - def silence_stderr #:nodoc: - ActiveSupport::Deprecation.warn( - "#silence_stderr is deprecated and will be removed in the next release" - ) #not thread-safe - silence_stream(STDERR) { yield } - end - - # Deprecated : this method is not thread safe - # Silences any stream for the duration of the block. - # - # silence_stream(STDOUT) do - # puts 'This will never be seen' - # end - # - # puts 'But this will' - # - # This method is not thread-safe. - 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 - # Blocks and ignores any exception passed as argument if raised within the block. # # suppress(ZeroDivisionError) do @@ -69,56 +40,4 @@ module Kernel yield rescue *exception_classes end - - # Captures the given stream and returns it: - # - # stream = capture(:stdout) { puts 'notice' } - # stream # => "notice\n" - # - # stream = capture(:stderr) { warn 'error' } - # stream # => "error\n" - # - # even for subprocesses: - # - # stream = capture(:stdout) { system('echo notice') } - # stream # => "notice\n" - # - # stream = capture(:stderr) { system('echo error 1>&2') } - # stream # => "error\n" - def capture(stream) - ActiveSupport::Deprecation.warn( - "#capture(stream) is deprecated and will be removed in the next release" - ) #not thread-safe - 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 - alias :silence :capture - - # Silences both STDOUT and STDERR, even for subprocesses. - # - # quietly { system 'bundle install' } - # - # This method is not thread-safe. - def quietly - ActiveSupport::Deprecation.warn( - "#quietly is deprecated and will be removed in the next release" - ) #not thread-safe - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end end diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index 768b980f21..d9fb392752 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation/proxy_wrappers' + class LoadError REGEXPS = [ /^no such file to load -- (.+)$/i, @@ -25,4 +27,4 @@ class LoadError end end -MissingSourceFile = LoadError +MissingSourceFile = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('MissingSourceFile', 'LoadError') diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 580cb80413..0a6fadf928 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -19,9 +19,9 @@ class Module # alias_method :foo_without_feature?, :foo? # alias_method :foo?, :foo_with_feature? # - # so you can safely chain foo, foo?, and foo! with the same feature. + # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) - # Strip out punctuation on predicates or bang methods since + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 yield(aliased_target, punctuation) if block_given? diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index 67f0e0335d..93fb598650 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -27,11 +27,8 @@ class Module def attr_internal_define(attr_name, type) internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '') - # class_eval is necessary on 1.9 or else the methods are made private - class_eval do - # use native attr_* methods as they are faster on some Ruby implementations - send("attr_#{type}", internal_name) - end + # use native attr_* methods as they are faster on some Ruby implementations + send("attr_#{type}", internal_name) attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer alias_method attr_name, internal_name remove_method internal_name diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index d317df5079..d4e6b5a1ac 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -203,7 +203,7 @@ class Module # include HairColors # end # - # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) mattr_writer(*syms, &blk) diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index e926392952..a5f4d03256 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,15 +1,23 @@ +require 'set' + class Module # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ # option is not used. class DelegationError < NoMethodError; end + RUBY_RESERVED_WORDS = Set.new( + %w(alias and BEGIN begin break case class def defined? do else elsif END + end ensure false for if in module next nil not or redo rescue retry + return self super then true undef unless until when while yield) + ).freeze + # Provides a +delegate+ class method to easily expose contained objects' # public methods as your own. # # ==== Options # * <tt>:to</tt> - Specifies the target object # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ to be raised + # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option @@ -163,7 +171,7 @@ class Module line = line.to_i to = to.to_s - to = 'self.class' if to == 'class' + to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= @@ -177,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/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb index b1097cc83b..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb +++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb @@ -1,11 +1,3 @@ -class Module - ### - # TODO: remove this after 1.9 support is dropped - def methods_transplantable? # :nodoc: - x = Module.new { def foo; end } - Module.new { define_method :bar, x.instance_method(:foo) } - true - rescue TypeError - false - end -end +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 719071d1c2..8a2569a7d0 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -1,10 +1,13 @@ class Module + # Remove 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. 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 e1ebd4f91c..6b447d772b 100644 --- a/activesupport/lib/active_support/core_ext/name_error.rb +++ b/activesupport/lib/active_support/core_ext/name_error.rb @@ -1,5 +1,12 @@ class NameError # Extract the name of the missing constant from the exception message. + # + # begin + # HelloWorld + # rescue NameError => e + # e.missing_name + # end + # # => "HelloWorld" def missing_name if /undefined local variable or method/ !~ message $1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message @@ -7,10 +14,16 @@ class NameError end # Was this exception raised because the given name was missing? + # + # begin + # HelloWorld + # rescue NameError => e + # e.missing_name?("HelloWorld") + # end + # # => 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/bytes.rb b/activesupport/lib/active_support/core_ext/numeric/bytes.rb index deea8e9358..dfbca32474 100644 --- a/activesupport/lib/active_support/core_ext/numeric/bytes.rb +++ b/activesupport/lib/active_support/core_ext/numeric/bytes.rb @@ -7,36 +7,56 @@ class Numeric EXABYTE = PETABYTE * 1024 # Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes + # + # 2.bytes # => 2 def bytes self end alias :byte :bytes + # Returns the number of bytes equivalent to the kilobytes provided. + # + # 2.kilobytes # => 2048 def kilobytes self * KILOBYTE end alias :kilobyte :kilobytes + # Returns the number of bytes equivalent to the megabytes provided. + # + # 2.megabytes # => 2_097_152 def megabytes self * MEGABYTE end alias :megabyte :megabytes + # Returns the number of bytes equivalent to the gigabytes provided. + # + # 2.gigabytes # => 2_147_483_648 def gigabytes self * GIGABYTE end alias :gigabyte :gigabytes + # Returns the number of bytes equivalent to the terabytes provided. + # + # 2.terabytes # => 2_199_023_255_552 def terabytes self * TERABYTE end alias :terabyte :terabytes + # Returns the number of bytes equivalent to the petabytes provided. + # + # 2.petabytes # => 2_251_799_813_685_248 def petabytes self * PETABYTE end alias :petabyte :petabytes + # Returns the number of bytes equivalent to the exabytes provided. + # + # 2.exabytes # => 2_305_843_009_213_693_952 def exabytes self * EXABYTE end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 6d3635c69a..0c8ff79237 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -118,18 +118,28 @@ class Numeric end end - [Float, Fixnum, Bignum, BigDecimal].each do |klass| - klass.send(:alias_method, :to_default_s, :to_s) - - klass.send(:define_method, :to_s) do |*args| - if args[0].is_a?(Symbol) - format = args[0] - options = args[1] || {} + [Fixnum, Bignum].each do |klass| + klass.class_eval do + alias_method :to_default_s, :to_s + def to_s(base_or_format = 10, options = nil) + if base_or_format.is_a?(Symbol) + to_formatted_s(base_or_format, options || {}) + else + to_default_s(base_or_format) + end + end + end + end - self.to_formatted_s(format, options) + Float.class_eval do + alias_method :to_default_s, :to_s + def to_s(*args) + if args.empty? + to_default_s else - to_default_s(*args) + to_formatted_s(*args) end end 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 689fae4830..98716383f4 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. @@ -36,33 +38,51 @@ class Numeric end alias :second :seconds + # Returns a Duration instance matching the number of minutes provided. + # + # 2.minutes # => 120 seconds def minutes ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]]) end alias :minute :minutes + # Returns a Duration instance matching the number of hours provided. + # + # 2.hours # => 7_200 seconds def hours ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]]) end alias :hour :hours + # Returns a Duration instance matching the number of days provided. + # + # 2.days # => 2 days def days ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) end alias :day :days + # Returns a Duration instance matching the number of weeks provided. + # + # 2.weeks # => 14 days def weeks ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) end alias :week :weeks + # Returns a Duration instance matching the number of fortnights provided. + # + # 2.fortnights # => 28 days def fortnights ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]]) end alias :fortnight :fortnights + # Returns the number of milliseconds equivalent to the seconds provided. # Used with the standard time durations, like 1.hour.in_milliseconds -- # so we can feed them to JavaScript functions like getTime(). + # + # 2.in_milliseconds # => 2_000 def in_milliseconds self * 1000 end diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index f1106cca9b..f4f9152d6a 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -2,7 +2,6 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/deep_dup' -require 'active_support/core_ext/object/itself' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/inclusion' 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 2e99f4a1b8..0191d2e973 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -25,7 +25,7 @@ class Array # array[1][2] # => nil # dup[1][2] # => 4 def deep_dup - map { |it| it.deep_dup } + map(&:deep_dup) end end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 665cb0f96d..620f7b6561 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects; + # False for +nil+, +false+, +true+, symbol, number objects; # true otherwise. def duplicable? true @@ -78,17 +78,8 @@ end require 'bigdecimal' class BigDecimal - # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead - # raises TypeError exception. Checking here on the runtime whether BigDecimal - # will allow dup or not. - begin - BigDecimal.new('4.56').dup - - def duplicable? - true - end - rescue TypeError - # can't dup, so use superclass implementation + def duplicable? + true end end diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb index 755e1c6b16..593a7a4940 100644 --- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb +++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb @@ -23,6 +23,6 @@ class Object # # C.new(0, 1).instance_variable_names # => ["@y", "@x"] def instance_variable_names - instance_variables.map { |var| var.to_s } + instance_variables.map(&:to_s) end end diff --git a/activesupport/lib/active_support/core_ext/object/itself.rb b/activesupport/lib/active_support/core_ext/object/itself.rb deleted file mode 100644 index d71cea6674..0000000000 --- a/activesupport/lib/active_support/core_ext/object/itself.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Object - # TODO: Remove this file when we drop support for Ruby < 2.2 - unless respond_to?(:itself) - # Returns the object itself. - # - # Useful for chaining methods, such as Active Record scopes: - # - # Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at) - # - # @return Object - def itself - self - end - end -end diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index ccd568bbf5..ec5ace4e16 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -38,7 +38,7 @@ class Array # Calls <tt>to_param</tt> on all its elements and joins the result with # slashes. This is used by <tt>url_for</tt> in Action Pack. def to_param - collect { |e| e.to_param }.join '/' + collect(&:to_param).join '/' end # Converts an array into a string suitable for use as a URL query string, diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 31919474ed..e0f70b9caa 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -9,7 +9,23 @@ class Object # # instead of # - # @person ? @person.name : nil + # @person.name if @person + # + # +try+ calls can be chained: + # + # @person.try(:spouse).try(:name) + # + # instead of + # + # @person.spouse.name if @person && @person.spouse + # + # +try+ will also return +nil+ if the receiver does not respond to the method: + # + # @person.try(:non_existing_method) # => nil + # + # instead of + # + # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil # # +try+ returns +nil+ when called on +nil+ regardless of whether it responds # to the method: @@ -24,7 +40,7 @@ class Object # # The number of arguments in the signature must match. If the object responds # to the method the call is attempted and +ArgumentError+ is still raised - # otherwise. + # in case of argument mismatch. # # If +try+ is called without arguments it yields the receiver to a given # block unless it is +nil+: @@ -38,12 +54,22 @@ class Object # # @person.try { upcase.truncate(50) } # - # Please also note that +try+ is defined on +Object+, therefore it won't work + # 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 - # delegator itself. + # the delegator itself. def try(*a, &b) + try!(*a, &b) if a.empty? || respond_to?(a.first) + end + + # 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) @@ -51,16 +77,6 @@ class Object yield self end else - public_send(*a, &b) if respond_to?(a.first) - end - end - - # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and - # does not implement the tried method. - def try!(*a, &b) - if a.empty? && block_given? - yield self - else public_send(*a, &b) end end @@ -68,12 +84,12 @@ end class NilClass # Calling +try+ on +nil+ always returns +nil+. - # It becomes specially helpful when navigating through associations that may return +nil+. + # It becomes especially helpful when navigating through associations that may return +nil+. # # nil.try(:name) # => nil # # Without +try+ - # @person && !@person.children.blank? && @person.children.first.name + # @person && @person.children.any? && @person.children.first.name # # With +try+ # @person.try(:children).try(:first).try(:name) @@ -81,6 +97,9 @@ class NilClass nil end + # Calling +try!+ on +nil+ always returns +nil+. + # + # nil.try!(:name) # => nil def try!(*args) nil end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 42e87c4424..7d38e1d134 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -47,7 +47,21 @@ class Object # end # # <tt>with_options</tt> can also be nested since the call is forwarded to its receiver. - # Each nesting level will merge inherited defaults in addition to their own. + # + # NOTE: Each nesting level will merge inherited defaults in addition to their own. + # + # class Post < ActiveRecord::Base + # with_options if: :persisted?, length: { minimum: 50 } do + # validates :content, if: -> { content.present? } + # end + # end + # + # The code is equivalent to: + # + # validates :content, length: { minimum: 50 }, if: -> { content.present? } + # + # Hence the inherited default for `if` key is ignored. + # def with_options(options, &block) option_merger = ActiveSupport::OptionMerger.new(self, options) block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) diff --git a/activesupport/lib/active_support/core_ext/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/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 1dfaf76673..b88976eab2 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -3,7 +3,7 @@ class String # the string, and then changing remaining consecutive whitespace # groups into one space each. # - # Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). + # Note that it handles both ASCII and Unicode whitespace. # # %{ Multi-line # string }.squish # => "Multi-line string" @@ -13,6 +13,9 @@ class String end # Performs a destructive squish. See String#squish. + # str = " foo bar \n \t boo" + # str.squish! # => "foo bar boo" + # str # => "foo bar boo" def squish! gsub!(/\A[[:space:]]+/, '') gsub!(/[[:space:]]+\z/, '') @@ -20,14 +23,24 @@ class String self end - # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, ''). - def remove(pattern) - gsub pattern, '' + # Returns a new string with all occurrences of the patterns removed. + # str = "foo bar test" + # str.remove(" test") # => "foo bar" + # str # => "foo bar test" + def remove(*patterns) + dup.remove!(*patterns) end - # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, ''). - def remove!(pattern) - gsub! pattern, '' + # Alters the string by removing all occurrences of the patterns. + # str = "foo bar test" + # str.remove!(" test") # => "foo bar" + # str # => "foo bar" + def remove!(*patterns) + patterns.each do |pattern| + gsub! pattern, "" + end + + self end # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>: @@ -80,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 a943752f17..38d567c014 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -199,6 +199,7 @@ class String # 'employee_salary'.humanize # => "Employee salary" # 'author_id'.humanize # => "Author" # 'author_id'.humanize(capitalize: false) # => "author" + # '_id'.humanize # => "Id" def humanize(options = {}) ActiveSupport::Inflector.humanize(self, options) end diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index a124202936..2eedd4fdb1 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 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 c761325108..bae4e206e6 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -1,6 +1,5 @@ require 'erb' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/deprecation' class ERB module Util @@ -86,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: @@ -150,7 +154,11 @@ module ActiveSupport #:nodoc: else if html_safe? new_safe_buffer = super - new_safe_buffer.instance_eval { @html_safe = true } + + if new_safe_buffer + new_safe_buffer.instance_variable_set :@html_safe, true + end + new_safe_buffer else to_str[*args] @@ -186,11 +194,6 @@ module ActiveSupport #:nodoc: super(html_escape_interpolated_argument(value)) end - def prepend!(value) - ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend - prepend value - end - def +(other) dup.concat(other) end @@ -219,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| @@ -247,6 +250,11 @@ module ActiveSupport #:nodoc: end class String + # Marks a string as trusted safe. It will be inserted into HTML with no + # additional escaping performed. It is your responsibilty to ensure that the + # string contains no malicious content. This method is equivalent to the + # `raw` helper in views. It is recommended that you use `sanitize` instead of + # this method. It should never be called on user input. def html_safe ActiveSupport::SafeBuffer.new(self) end diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb index c2c30044f2..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/struct.rb +++ b/activesupport/lib/active_support/core_ext/struct.rb @@ -1,6 +1,3 @@ -# Backport of Struct#to_h from Ruby 2.0 -class Struct # :nodoc: - def to_h - Hash[members.zip(values)] - end -end unless Struct.instance_methods.include?(:to_h) +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb deleted file mode 100644 index 4cd6634558..0000000000 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ /dev/null @@ -1,86 +0,0 @@ -class Thread - LOCK = Mutex.new # :nodoc: - - # Returns the value of a thread local variable that has been set. Note that - # these are different than fiber local values. - # - # Thread local values are carried along with threads, and do not respect - # fibers. For example: - # - # Thread.new { - # Thread.current.thread_variable_set("foo", "bar") # set a thread local - # Thread.current["foo"] = "bar" # set a fiber local - # - # Fiber.new { - # Fiber.yield [ - # Thread.current.thread_variable_get("foo"), # get the thread local - # Thread.current["foo"], # get the fiber local - # ] - # }.resume - # }.join.value # => ['bar', nil] - # - # The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned - # for the fiber local. The fiber is executed in the same thread, so the - # thread local values are available. - def thread_variable_get(key) - _locals[key.to_sym] - end - - # Sets a thread local with +key+ to +value+. Note that these are local to - # threads, and not to fibers. Please see Thread#thread_variable_get for - # more information. - def thread_variable_set(key, value) - _locals[key.to_sym] = value - end - - # Returns an array of the names of the thread-local variables (as Symbols). - # - # thr = Thread.new do - # Thread.current.thread_variable_set(:cat, 'meow') - # Thread.current.thread_variable_set("dog", 'woof') - # end - # thr.join # => #<Thread:0x401b3f10 dead> - # thr.thread_variables # => [:dog, :cat] - # - # Note that these are not fiber local variables. Please see Thread#thread_variable_get - # for more details. - def thread_variables - _locals.keys - end - - # Returns <tt>true</tt> if the given string (or symbol) exists as a - # thread-local variable. - # - # me = Thread.current - # me.thread_variable_set(:oliver, "a") - # me.thread_variable?(:oliver) # => true - # me.thread_variable?(:stanley) # => false - # - # Note that these are not fiber local variables. Please see Thread#thread_variable_get - # for more details. - def thread_variable?(key) - _locals.has_key?(key.to_sym) - end - - # Freezes the thread so that thread local variables cannot be set via - # Thread#thread_variable_set, nor can fiber local variables be set. - # - # me = Thread.current - # me.freeze - # me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals - # me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals - def freeze - _locals.freeze - super - end - - private - - def _locals - if defined?(@_locals) - @_locals - else - LOCK.synchronize { @_locals ||= {} } - end - end -end unless Thread.instance_methods.include?(:thread_variable_set) diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb index 32cffe237d..72c3234630 100644 --- a/activesupport/lib/active_support/core_ext/time.rb +++ b/activesupport/lib/active_support/core_ext/time.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/time/acts_like' require 'active_support/core_ext/time/calculations' require 'active_support/core_ext/time/conversions' -require 'active_support/core_ext/time/marshal' require 'active_support/core_ext/time/zones' diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 6a7bf7445a..13610ba19f 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -64,11 +64,12 @@ class Time # Returns a new Time where one or more of the elements have been changed according # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed, - # then minute, sec, and usec is set to 0. If the hour and minute is passed, then - # sec and usec 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>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only + # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> + # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # 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) @@ -80,7 +81,13 @@ class Time new_hour = options.fetch(:hour, hour) new_min = options.fetch(:min, options[:hour] ? 0 : min) new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) - new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + + if new_nsec = options[:nsec] + raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] + new_usec = Rational(new_nsec, 1000) + else + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + end if utc? ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) @@ -155,7 +162,7 @@ class Time alias :at_noon :middle_of_day alias :at_middle_of_day :middle_of_day - # Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9) + # Returns a new Time representing the end of the day, 23:59:59.999999 def end_of_day change( :hour => 23, @@ -172,7 +179,7 @@ class Time end alias :at_beginning_of_hour :beginning_of_hour - # Returns a new Time representing the end of the hour, x:59:59.999999 (.999999999 in ruby1.9) + # Returns a new Time representing the end of the hour, x:59:59.999999 def end_of_hour change( :min => 59, @@ -188,7 +195,7 @@ class Time end alias :at_beginning_of_minute :beginning_of_minute - # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9) + # Returns a new Time representing the end of the minute, x:xx:59.999999 def end_of_minute change( :sec => 59, diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb index 497c4c3fb8..467bad1726 100644 --- a/activesupport/lib/active_support/core_ext/time/marshal.rb +++ b/activesupport/lib/active_support/core_ext/time/marshal.rb @@ -1,30 +1,3 @@ -# Ruby 1.9.2 adds utc_offset and zone to Time, but marshaling only -# preserves utc_offset. Preserve zone also, even though it may not -# work in some edge cases. -if Time.local(2010).zone != Marshal.load(Marshal.dump(Time.local(2010))).zone - class Time - class << self - alias_method :_load_without_zone, :_load - def _load(marshaled_time) - time = _load_without_zone(marshaled_time) - time.instance_eval do - if zone = defined?(@_zone) && remove_instance_variable('@_zone') - ary = to_a - ary[0] += subsec if ary[0] == sec - ary[-1] = zone - utc? ? Time.utc(*ary) : Time.local(*ary) - else - self - end - end - end - end +require 'active_support/deprecation' - alias_method :_dump_without_zone, :_dump - def _dump(*args) - obj = dup - obj.instance_variable_set('@_zone', zone) - obj.send :_dump_without_zone, *args - end - end -end +ActiveSupport::Deprecation.warn("This is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index bbda04d60c..d683e7c777 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -1,4 +1,5 @@ require 'active_support/time_with_zone' +require 'active_support/core_ext/time/acts_like' require 'active_support/core_ext/date_and_time/zones' class Time @@ -25,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? @@ -50,7 +51,16 @@ class Time end end - # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones. + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Raises an ArgumentError for invalid time zones. + # + # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...> + # Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...> + # Time.find_zone! nil # => nil + # Time.find_zone! false # => false + # Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE def find_zone!(time_zone) if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone @@ -71,6 +81,12 @@ class Time raise ArgumentError, "Invalid Timezone: #{time_zone}" end + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Returns +nil+ for invalid time zones. + # + # Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone "NOT-A-TIMEZONE" # => nil def find_zone(time_zone) find_zone!(time_zone) rescue nil end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 93a11d4586..664cc15a29 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -30,6 +30,10 @@ module ActiveSupport #:nodoc: mattr_accessor :loaded self.loaded = Set.new + # Stack of files being loaded. + mattr_accessor :loading + self.loading = [] + # Should we load files or require them? mattr_accessor :mechanism self.mechanism = ENV['NO_RELOAD'] ? :require : :load @@ -201,7 +205,10 @@ module ActiveSupport #:nodoc: # Object includes this module. module Loadable #:nodoc: def self.exclude_from(base) - base.class_eval { define_method(:load, Kernel.instance_method(:load)) } + base.class_eval do + define_method(:load, Kernel.instance_method(:load)) + private :load + end end def require_or_load(file_name) @@ -237,18 +244,6 @@ module ActiveSupport #:nodoc: raise end - def load(file, wrap = false) - result = false - load_dependency(file) { result = super } - result - end - - def require(file) - result = false - load_dependency(file) { result = super } - result - end - # Mark the given constant as unloadable. Unloadable constants are removed # each time dependencies are cleared. # @@ -265,6 +260,20 @@ module ActiveSupport #:nodoc: def unloadable(const_desc) Dependencies.mark_for_unload const_desc end + + private + + def load(file, wrap = false) + result = false + load_dependency(file) { result = super } + result + end + + def require(file) + result = false + load_dependency(file) { result = super } + result + end end # Exception file-blaming. @@ -317,6 +326,7 @@ module ActiveSupport #:nodoc: def clear log_call loaded.clear + loading.clear remove_unloadable_constants! end @@ -329,6 +339,7 @@ module ActiveSupport #:nodoc: # Record that we've seen this file *before* loading it to avoid an # infinite loop with mutual dependencies. loaded << expanded + loading << expanded begin if load? @@ -351,6 +362,8 @@ module ActiveSupport #:nodoc: rescue Exception loaded.delete expanded raise + ensure + loading.pop end # Record history *after* loading so first load gets warnings. @@ -360,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 @@ -408,7 +421,7 @@ module ActiveSupport #:nodoc: end def load_once_path?(path) - # to_s works around a ruby1.9 issue where String#starts_with?(Pathname) + # to_s works around a ruby issue where String#starts_with?(Pathname) # will raise a TypeError: no implicit conversion of Pathname into String autoload_once_paths.any? { |base| path.starts_with? base.to_s } end @@ -475,7 +488,7 @@ module ActiveSupport #:nodoc: expanded = File.expand_path(file_path) expanded.sub!(/\.rb\z/, '') - if loaded.include?(expanded) + if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else require_or_load(expanded, qualified_name) @@ -594,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 @@ -729,7 +742,7 @@ module ActiveSupport #:nodoc: protected def log_call(*args) if log_activity? - arg_str = args.collect { |arg| arg.inspect } * ', ' + arg_str = args.collect(&:inspect) * ', ' /in `([a-z_\?\!]+)'/ =~ caller(1).first selector = $1 || '<unknown>' log "called #{selector}(#{arg_str})" diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index c0dba5f7fd..13036d521d 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -67,7 +67,7 @@ module ActiveSupport end def eager_load! - @_autoloads.values.each { |file| require file } + @_autoloads.each_value { |file| require file } end def autoloads diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 328b8c320a..9f9dca8453 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -20,7 +20,7 @@ module ActiveSupport log: ->(message, callstack) { logger = - if defined?(Rails) && Rails.logger + if defined?(Rails.logger) && Rails.logger Rails.logger else require 'active_support/logger' diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 1a6c02a39b..5a64fc52cc 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -1,4 +1,3 @@ -require 'active_support/proxy_object' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/object/acts_like' @@ -7,7 +6,7 @@ module ActiveSupport # Time#advance, respectively. It mainly supports the methods on Numeric. # # 1.month.ago # equivalent to Time.now.advance(months: -1) - class Duration < ProxyObject + class Duration attr_accessor :value, :parts def initialize(value, parts) #:nodoc: @@ -53,8 +52,18 @@ module ActiveSupport end end + def to_s + @value.to_s + end + + # Returns +true+ if +other+ is also a Duration instance, which has the + # same parts as this one. def eql?(other) - other.is_a?(Duration) && self == other + Duration === other && other.value.eql?(value) + end + + def hash + @value.hash end def self.===(other) #:nodoc: @@ -82,13 +91,19 @@ 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: to_i end + def respond_to_missing?(method, include_private=false) #:nodoc: + @value.respond_to?(method, include_private) + end + + delegate :<=>, to: :value + protected def sum(sign, time = ::Time.current) #:nodoc: @@ -107,13 +122,6 @@ module ActiveSupport private - # We define it as a workaround to Ruby 2.0.0-p353 bug. - # For more information, check rails/rails#13055. - # Remove it when we drop support for 2.0.0-p353. - def ===(other) #:nodoc: - value === other - end - def method_missing(method, *args, &block) #:nodoc: value.send(method, *args, &block) end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 0a2b4c1618..7068f09d87 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -5,10 +5,10 @@ module ActiveSupport end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 3d8f2d572b..4f71f13971 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/hash/reverse_merge' module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -266,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 affcfb7398..95f3f6255a 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -55,14 +55,20 @@ module I18n reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } app.reloaders << reloader - ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } + ActionDispatch::Reloader.to_prepare do + reloader.execute_if_updated + # TODO: remove the following line as soon as the return value of + # callbacks is ignored, that is, returning `false` does not + # display a deprecation warning or halts the callback chain. + true + end reloader.execute @i18n_inited = true 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/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 97401ccec7..486838bd15 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -154,7 +154,7 @@ module ActiveSupport end end - # Add uncountable words that shouldn't be attempted inflected. + # Specifies words that are uncountable and should not be inflected. # # uncountable 'money' # uncountable 'money', 'information' diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index f35e71ce81..fe8a2ac9ba 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -22,49 +22,49 @@ 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 @@ -73,7 +73,7 @@ module ActiveSupport string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!('/', '::') + string.gsub!(/\//, '::') string end @@ -81,17 +81,17 @@ 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.gsub!(/(?:([A-Za-z\d])|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + word = camel_cased_word.to_s.gsub(/::/, '/') + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") @@ -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 } 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,9 +222,9 @@ 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 @@ -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('Test::Unit') # => Test::Unit # # 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 8b5fc70dee..35548f3f56 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -12,7 +12,7 @@ module ActiveSupport class << self # Parses a JSON string (JavaScript Object Notation) into a hash. - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index f29d42276d..48f4967892 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -6,14 +6,13 @@ module ActiveSupport delegate :use_standard_json_time_format, :use_standard_json_time_format=, :time_precision, :time_precision=, :escape_html_entities_in_json, :escape_html_entities_in_json=, - :encode_big_decimal_as_string, :encode_big_decimal_as_string=, :json_encoder, :json_encoder=, :to => :'ActiveSupport::JSON::Encoding' end module JSON # Dumps objects in JSON (JavaScript Object Notation). - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.encode({ team: 'rails', players: '36' }) # # => "{\"team\":\"rails\",\"players\":\"36\"}" @@ -113,54 +112,6 @@ module ActiveSupport # Sets the encoder used by Rails to encode Ruby objects into JSON strings # in +Object#to_json+ and +ActiveSupport::JSON.encode+. attr_accessor :json_encoder - - def encode_big_decimal_as_string=(as_string) - message = \ - "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ - "the new encoder will always encode them as strings.\n\n" \ - "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \ - "your configuration file. If you have been setting this to true, you can safely remove it from " \ - "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \ - "Gemfile in order to restore this functionality." - - raise NotImplementedError, message - end - - def encode_big_decimal_as_string - message = \ - "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ - "the new encoder will always encode them as strings.\n\n" \ - "You are seeing this error because you are trying to check the value of the related configuration, " \ - "'active_support.encode_big_decimal_as_string'. If your application depends on this option, you should " \ - "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \ - "In the future, it will be removed from Rails, so you should stop checking its value." - - ActiveSupport::Deprecation.warn message - - true - end - - # Deprecate CircularReferenceError - def const_missing(name) - if name == :CircularReferenceError - message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \ - "You are seeing this warning because you are rescuing from (or otherwise referencing) " \ - "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \ - "removed from Rails. You should remove these rescue blocks from your code and ensure " \ - "that your data structures are free of circular references so they can be properly " \ - "serialized into JSON.\n\n" \ - "For example, the following Hash contains a circular reference to itself:\n" \ - " h = {}\n" \ - " h['circular'] = h\n" \ - "In this case, calling h.to_json would not work properly." - - ActiveSupport::Deprecation.warn message - - SystemStackError - else - super - end - end end self.use_standard_json_time_format = true diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 6cb2884fb7..eee9bbaead 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -1,5 +1,6 @@ require 'base64' require 'active_support/core_ext/object/blank' +require 'active_support/security_utils' module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are @@ -33,37 +34,90 @@ module ActiveSupport @serializer = options[:serializer] || Marshal end - def verify(signed_message) - raise InvalidSignature if signed_message.blank? + # Checks if a signed message could have been generated by signing an object + # with the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # verifier.valid_message?(signed_message) # => true + # + # tampered_message = signed_message.chop # editing the message invalidates the signature + # verifier.valid_message?(tampered_message) # => false + def valid_message?(signed_message) + return if signed_message.blank? data, digest = signed_message.split("--") - if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) + data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) + end + + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # + # signed_message = verifier.generate 'a private message' + # verifier.verified(signed_message) # => 'a private message' + # + # Returns +nil+ if the message was not signed with the same secret. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verified(signed_message) # => nil + # + # Returns +nil+ if the message is not Base64-encoded. + # + # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d" + # verifier.verified(invalid_message) # => nil + # + # Raises any error raised while decoding the signed message. + # + # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff" + # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format + def verified(signed_message) + if valid_message?(signed_message) begin - @serializer.load(::Base64.strict_decode64(data)) + data = signed_message.split("--")[0] + @serializer.load(decode(data)) rescue ArgumentError => argument_error - raise InvalidSignature if argument_error.message =~ %r{invalid base64} + return if argument_error.message =~ %r{invalid base64} raise end - else - raise InvalidSignature end end + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # + # verifier.verify(signed_message) # => 'a private message' + # + # Raises +InvalidSignature+ if the message was not signed with the same + # secret or was not Base64-encoded. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature + def verify(signed_message) + verified(signed_message) || raise(InvalidSignature) + end + + # Generates a signed message for the provided value. + # + # The message is signed with the +MessageVerifier+'s secret. Without knowing + # the secret, the original value cannot be extracted from the message. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" def generate(value) - data = ::Base64.strict_encode64(@serializer.dump(value)) + data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private - # constant-time comparison algorithm to prevent timing attacks - def secure_compare(a, b) - return false unless a.bytesize == b.bytesize - - l = a.unpack "C#{a.bytesize}" + def encode(data) + ::Base64.strict_encode64(data) + end - res = 0 - b.each_byte { |byte| res |= byte ^ l.shift } - res == 0 + def decode(data) + ::Base64.strict_decode64(data) end def generate_digest(data) diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 89009d1f55..35efebc65f 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.3.0' + UNICODE_VERSION = '7.0.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -42,7 +42,6 @@ module ActiveSupport 0x0085, # White_Space # Cc <control-0085> 0x00A0, # White_Space # Zs NO-BREAK SPACE 0x1680, # White_Space # Zs OGHAM SPACE MARK - 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE 0x2028, # White_Space # Zl LINE SEPARATOR 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR @@ -212,9 +211,8 @@ module ActiveSupport codepoints end - # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. - if '<3'.respond_to?(:scrub) && !defined?(Rubinius) + if !defined?(Rubinius) # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent # resulting in a valid UTF-8 string. # diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 325a3d75dc..b9f8e1ab2c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -16,7 +16,7 @@ module ActiveSupport # render text: 'Foo' # end # - # That executes the block first and notifies all subscribers once done. + # That first executes the block and then notifies all subscribers once done. # # In the example above +render+ is the name of the event, and the rest is called # the _payload_. The payload is a mechanism that allows instrumenters to pass diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 3a244b34b5..075ddc2382 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -57,6 +57,18 @@ module ActiveSupport @duration = nil end + # Returns the difference in milliseconds between when the execution of the + # event started and when it ended. + # + # ActiveSupport::Notifications.subscribe('wait') do |*args| + # @event = ActiveSupport::Notifications::Event.new(*args) + # end + # + # ActiveSupport::Notifications.instrument('wait') do + # sleep 1 + # end + # + # @event.duration # => 1000.138 def duration @duration ||= 1000.0 * (self.end - time) end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 5ecda9593a..cfca42bc69 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -94,7 +94,7 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). + # (defaults to 3). Keeps the number's precision if nil. # * <tt>:significant</tt> - If +true+, precision will be the # # of significant_digits. If +false+, the # of fractional # digits (defaults to +false+). @@ -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,7 +162,7 @@ module ActiveSupport # * <tt>:locale</tt> - Sets the locale to be used for formatting # (defaults to current locale). # * <tt>:precision</tt> - Sets the precision of the number - # (defaults to 3). + # (defaults to 3). Keeps the number's precision if nil. # * <tt>:significant</tt> - If +true+, precision will be the # # of significant_digits. If +false+, the # of fractional # digits (defaults to +false+). @@ -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) @@ -272,12 +274,12 @@ module ActiveSupport # string containing an i18n scope where to find this hash. It # might have the following keys: # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, - # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, - # *<tt>:billion</tt>, <tt>:trillion</tt>, - # *<tt>:quadrillion</tt> + # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # <tt>:billion</tt>, <tt>:trillion</tt>, + # <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, - # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, - # *<tt>:pico</tt>, <tt>:femto</tt> + # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string # (defaults to "%n %u"). The field types are: # * %u - The quantifier (ex.: 'thousand') 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 9ae27a896a..fb5adb574a 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/, rounded_number).gsub(/%u/, options[:unit]) end private 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 9a3dc526ae..6940beb318 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 @@ -59,7 +59,7 @@ module ActiveSupport translate_in_locale("human.decimal_units.units", raise: true) else raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by { |e| -e } + end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@) end end end 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 eafe2844f7..1af294a03e 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/, 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..df316a08e6 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 = 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/railtie.rb b/activesupport/lib/active_support/railtie.rb index 133aa6a054..6eba24b569 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,6 +13,13 @@ 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| diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index a7eba91ac5..1a02acd5b1 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -60,7 +60,7 @@ module ActiveSupport end klasses.each do |klass| - key = if klass.is_a?(Class) && klass <= Exception + key = if klass.is_a?(Module) && klass.respond_to?(:===) klass.name elsif klass.is_a?(String) klass @@ -101,7 +101,7 @@ module ActiveSupport # itself when rescue_from CONSTANT is executed. klass = self.class.const_get(klass_name) rescue nil klass ||= klass_name.constantize rescue nil - exception.is_a?(klass) if klass + klass === exception if klass end case rescuer diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb new file mode 100644 index 0000000000..64c4801179 --- /dev/null +++ b/activesupport/lib/active_support/security_utils.rb @@ -0,0 +1,20 @@ +module ActiveSupport + module SecurityUtils + # Constant time string comparison. + # + # The values compared should be of fixed length, such as strings + # that have already been processed by HMAC. This should not be used + # on variable length plaintext strings because it could leak length info + # via timing attacks. + def secure_compare(a, b) + return false unless a.bytesize == b.bytesize + + l = a.unpack "C#{a.bytesize}" + + res = 0 + b.each_byte { |byte| res |= byte ^ l.shift } + res == 0 + end + module_function :secure_compare + end +end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 98be78b41b..cd40284660 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -96,7 +96,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 d5c2222d2e..9086a959aa 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -43,7 +43,9 @@ module ActiveSupport end def current_tags - Thread.current[:activesupport_tagged_logging_tags] ||= [] + # We use our object ID here to void conflicting with other instances + thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze + Thread.current[thread_key] ||= [] end private diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 4c3e77b7fd..739823bd56 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -8,37 +8,38 @@ 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/core_ext/kernel/reporting' -require 'active_support/deprecation' module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion class << self + # Sets the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order = :random # => :random + # + # Valid values are: + # * +:random+ (to run tests in random order) + # * +:parallel+ (to run tests in parallel) + # * +:sorted+ (to run tests alphabetically by method name) + # * +:alpha+ (equivalent to +:sorted+) def test_order=(new_order) ActiveSupport.test_order = new_order end + # Returns the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order # => :random + # + # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. + # Defaults to +:random+. def test_order test_order = ActiveSupport.test_order if test_order.nil? - ActiveSupport::Deprecation.warn "You did not specify a value for the " \ - "configuration option 'active_support.test_order'. In Rails 5.0, " \ - "the default value of this option will change from `:sorted` to " \ - "`:random`.\n" \ - "To disable this warning and keep the current behavior, you can add " \ - "the following line to your `config/environments/test.rb`:\n" \ - "\n" \ - " Rails.application.configure do\n" \ - " config.active_support.test_order = :sorted\n" \ - " end\n" \ - "\n" \ - "Alternatively, you can opt into the future behavior by setting this " \ - "option to `:random`." - - test_order = :sorted + test_order = :random self.test_order = test_order end @@ -55,6 +56,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 diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 11cca82995..8b649c193f 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -66,7 +66,7 @@ module ActiveSupport exps = expressions.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } - before = exps.map { |e| e.call } + before = exps.map(&:call) yield 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..247df7423b 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 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 1112c6e0b1..df5186ddec 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -90,7 +90,7 @@ module ActiveSupport # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # end # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 - def travel_to(date_or_time, &block) + def travel_to(date_or_time) if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime) now = date_or_time.midnight.to_time else @@ -99,10 +99,11 @@ 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 - block.call + yield ensure travel_back end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 4a0ed356b1..8ddf233b3e 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -75,8 +75,8 @@ module ActiveSupport # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your # system's <tt>ENV['TZ']</tt> zone. - def localtime - utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal + def localtime(utc_offset = nil) + utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset) end alias_method :getlocal, :localtime @@ -121,16 +121,25 @@ module ActiveSupport utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon) end - # Time uses +zone+ to display the time zone abbreviation, so we're - # duck-typing it. + # Returns the time zone abbreviation. + # + # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" + # Time.zone.now.zone # => "EST" def zone period.zone_identifier.to_s end + # Returns a string of the object's date, time, zone and offset from UTC. + # + # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00" def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end + # Returns a string of the object's date and time in the ISO 8601 standard + # format. + # + # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) fraction = if fraction_digits.to_i > 0 (".%06i" % time.usec)[0, fraction_digits.to_i + 1] @@ -187,7 +196,7 @@ module ActiveSupport # Returns a string of the object's date and time. # Accepts an optional <tt>format</tt>: - # * <tt>:default</tt> - default value, mimics Ruby 1.9 Time#to_s format. + # * <tt>:default</tt> - default value, mimics Ruby Time#to_s format. # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db). # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb. def to_s(format = :default) @@ -196,20 +205,16 @@ module ActiveSupport elsif formatter = ::Time::DATE_FORMATS[format] formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter) else - "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format + "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format end end alias_method :to_formatted_s, :to_s - # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and - # +formatted_offset+, respectively, before passing to Time#strftime, so - # that zone information is correct + # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime, + # so that zone information is correct. def strftime(format) - format = format.gsub('%Z', zone) - .gsub('%z', formatted_offset(false)) - .gsub('%:z', formatted_offset(true)) - .gsub('%::z', formatted_offset(true) + ":00") - time.strftime(format) + format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}") + getlocal(utc_offset).strftime(format) end # Use the time in UTC for comparisons. @@ -247,9 +252,23 @@ module ActiveSupport utc.hash end + # Adds an interval of time to the current object's time and return that + # value as a new TimeWithZone object. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now + 1000 # => Sun, 02 Nov 2014 01:43:08 EDT -04:00 + # + # If we're adding a Duration of variable length (i.e., years, months, days), + # move forward from #time, otherwise move forward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time + 24.hours will advance exactly 24 hours, while a + # time + 1.day will advance 23-25 hours, depending on the day. + # + # now + 24.hours # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now + 1.day # => Mon, 03 Nov 2014 01:26:28 EST -05:00 def +(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries if duration_of_variable_length?(other) method_missing(:+, other) else @@ -257,10 +276,25 @@ module ActiveSupport result.in_time_zone(time_zone) end end + alias_method :since, :+ + # Returns a new TimeWithZone object that represents the difference between + # the current object's time and the +other+ time. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EST -05:00 + # now - 1000 # => Sun, 02 Nov 2014 01:09:48 EST -05:00 + # + # If subtracting a Duration of variable length (i.e., years, months, days), + # move backward from #time, otherwise move backward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time - 24.hours will go subtract exactly 24 hours, while a + # time - 1.day will subtract 23-25 hours, depending on the day. + # + # now - 24.hours # => Sat, 01 Nov 2014 02:26:28 EDT -04:00 + # now - 1.day # => Sat, 01 Nov 2014 01:26:28 EDT -04:00 def -(other) - # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time, - # otherwise move backwards #utc, for accuracy when moving across DST boundaries if other.acts_like?(:time) to_time - other.to_time elsif duration_of_variable_length?(other) @@ -271,16 +305,6 @@ module ActiveSupport end end - def since(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries - if duration_of_variable_length?(other) - method_missing(:since, other) - else - utc.since(other).in_time_zone(time_zone) - end - end - def ago(other) since(-other) end @@ -307,15 +331,27 @@ module ActiveSupport [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end + # Returns the object's date and time as a floating point number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_f # => 1417709320.285418 def to_f utc.to_f end + # Returns the object's date and time as an integer number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_i # => 1417709320 def to_i utc.to_i end alias_method :tv_sec, :to_i + # Returns the object's date and time as a rational number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_r # => (708854548642709/500000) def to_r utc.to_r end diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 49dfee96ec..da39f0d245 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", @@ -184,7 +187,7 @@ module ActiveSupport } UTC_OFFSET_WITH_COLON = '%s%02d:%02d' - UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') + UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') @lazy_zones_map = ThreadSafe::Cache.new @@ -202,7 +205,7 @@ module ActiveSupport end def find_tzinfo(name) - TZInfo::TimezoneProxy.new(MAPPING[name] || name) + TZInfo::Timezone.new(MAPPING[name] || name) end alias_method :create, :new @@ -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 @@ -237,7 +233,7 @@ module ActiveSupport case arg when String begin - @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } + @lazy_zones_map[arg] ||= create(arg) rescue TZInfo::InvalidTimezoneIdentifier nil end @@ -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 diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 394ee95f4b..760be4c07a 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 27c64c4dca..f303daa1a7 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -141,7 +141,7 @@ module ActiveSupport (0...attributes.length).each do |i| attribute_hash[CONTENT_KEY] ||= '' attribute_hash[attributes.item(i).name] = attributes.item(i).value - end + end attribute_hash end diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 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/autoload_test.rb b/activesupport/test/autoload_test.rb index 7d02d835a8..c18b007612 100644 --- a/activesupport/test/autoload_test.rb +++ b/activesupport/test/autoload_test.rb @@ -11,6 +11,11 @@ class TestAutoloadModule < ActiveSupport::TestCase end end + def setup + @some_class_path = File.expand_path("test/fixtures/autoload/some_class.rb") + @another_class_path = File.expand_path("test/fixtures/autoload/another_class.rb") + end + test "the autoload module works like normal autoload" do module ::Fixtures::Autoload autoload :SomeClass, "fixtures/autoload/some_class" @@ -21,10 +26,12 @@ class TestAutoloadModule < ActiveSupport::TestCase test "when specifying an :eager constant it still works like normal autoload by default" do module ::Fixtures::Autoload - autoload :SomeClass, "fixtures/autoload/some_class" + eager_autoload do + autoload :SomeClass, "fixtures/autoload/some_class" + end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") + assert !$LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end @@ -33,16 +40,20 @@ class TestAutoloadModule < ActiveSupport::TestCase autoload :SomeClass end - assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") + assert !$LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end test "the location of :eager autoloaded constants defaults to :name.underscore" do module ::Fixtures::Autoload - autoload :SomeClass + eager_autoload do + autoload :SomeClass + end end + assert !$LOADED_FEATURES.include?(@some_class_path) ::Fixtures::Autoload.eager_load! + assert $LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end @@ -53,7 +64,7 @@ class TestAutoloadModule < ActiveSupport::TestCase end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb") + assert !$LOADED_FEATURES.include?(@another_class_path) assert_nothing_raised { ::Fixtures::AnotherClass } end @@ -64,7 +75,7 @@ class TestAutoloadModule < ActiveSupport::TestCase end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb") + assert !$LOADED_FEATURES.include?(@another_class_path) assert_nothing_raised { ::Fixtures::AnotherClass } end end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/typo.rb b/activesupport/test/autoloading_fixtures/typo.rb new file mode 100644 index 0000000000..8e047f5fd4 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/typo.rb @@ -0,0 +1,2 @@ +TypO = 1 + diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 5945605f7b..095e908907 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1021,6 +1021,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.tap { |l| p l } + end end class CacheEntryTest < ActiveSupport::TestCase @@ -1047,30 +1056,4 @@ class CacheEntryTest < ActiveSupport::TestCase assert_equal value, entry.value assert_equal value.bytesize, entry.size end - - def test_restoring_version_4beta1_entries - version_4beta1_entry = ActiveSupport::Cache::Entry.allocate - version_4beta1_entry.instance_variable_set(:@v, "hello") - version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i + 60) - entry = Marshal.load(Marshal.dump(version_4beta1_entry)) - assert_equal "hello", entry.value - assert_equal false, entry.expired? - end - - def test_restoring_compressed_version_4beta1_entries - version_4beta1_entry = ActiveSupport::Cache::Entry.allocate - version_4beta1_entry.instance_variable_set(:@v, Zlib::Deflate.deflate(Marshal.dump("hello"))) - version_4beta1_entry.instance_variable_set(:@c, true) - entry = Marshal.load(Marshal.dump(version_4beta1_entry)) - assert_equal "hello", entry.value - end - - def test_restoring_expired_version_4beta1_entries - version_4beta1_entry = ActiveSupport::Cache::Entry.allocate - version_4beta1_entry.instance_variable_set(:@v, "hello") - version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i - 1) - entry = Marshal.load(Marshal.dump(version_4beta1_entry)) - assert_equal "hello", entry.value - assert_equal true, entry.expired? - end end diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 32c2dfdfc0..f6abef8cee 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -49,7 +49,7 @@ module CallbacksTest def self.before(model) model.history << [:before_save, :class] end - + def self.after(model) model.history << [:after_save, :class] end @@ -501,21 +501,18 @@ module CallbacksTest end end - class CallbackTerminator + class AbstractCallbackTerminator include ActiveSupport::Callbacks - define_callbacks :save, :terminator => ->(_,result) { result == :halt } - - set_callback :save, :before, :first - set_callback :save, :before, :second - set_callback :save, :around, :around_it - set_callback :save, :before, :third - set_callback :save, :after, :first - set_callback :save, :around, :around_it - set_callback :save, :after, :second - set_callback :save, :around, :around_it - set_callback :save, :after, :third - + def self.set_save_callbacks + set_callback :save, :before, :first + set_callback :save, :before, :second + set_callback :save, :around, :around_it + set_callback :save, :before, :third + set_callback :save, :after, :first + set_callback :save, :around, :around_it + set_callback :save, :after, :third + end attr_reader :history, :saved, :halted def initialize @@ -552,6 +549,39 @@ module CallbacksTest end end + class CallbackTerminator < AbstractCallbackTerminator + define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt } + set_save_callbacks + end + + class CallbackTerminatorSkippingAfterCallbacks < AbstractCallbackTerminator + define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt }, + skip_after_callbacks_if_terminated: true + set_save_callbacks + end + + class CallbackDefaultTerminator < AbstractCallbackTerminator + define_callbacks :save + + def second + @history << "second" + throw(:abort) + end + + set_save_callbacks + end + + class CallbackFalseTerminator < AbstractCallbackTerminator + define_callbacks :save + + def second + @history << "second" + false + end + + set_save_callbacks + end + class CallbackObject def before(caller) caller.record << "before" @@ -688,10 +718,10 @@ module CallbacksTest end class CallbackTerminatorTest < ActiveSupport::TestCase - def test_termination + def test_termination_skips_following_before_and_around_callbacks terminator = CallbackTerminator.new terminator.save - assert_equal ["first", "second", "third", "second", "first"], terminator.history + assert_equal ["first", "second", "third", "first"], terminator.history end def test_termination_invokes_hook @@ -707,6 +737,73 @@ module CallbacksTest end end + class CallbackTerminatorSkippingAfterCallbacksTest < ActiveSupport::TestCase + def test_termination_skips_after_callbacks + terminator = CallbackTerminatorSkippingAfterCallbacks.new + terminator.save + assert_equal ["first", "second"], terminator.history + end + end + + class CallbackDefaultTerminatorTest < ActiveSupport::TestCase + def test_default_termination + terminator = CallbackDefaultTerminator.new + terminator.save + assert_equal ["first", "second", "third", "first"], terminator.history + end + + def test_default_termination_invokes_hook + terminator = CallbackDefaultTerminator.new + terminator.save + assert_equal :second, terminator.halted + end + + def test_block_never_called_if_abort_is_thrown + obj = CallbackDefaultTerminator.new + obj.save + assert !obj.saved + end + end + + class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase + def test_returning_false_halts_callback_if_config_variable_is_not_set + obj = CallbackFalseTerminator.new + assert_deprecated do + obj.save + assert_equal :second, obj.halted + assert !obj.saved + end + end + end + + class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase + def setup + ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true + end + + def test_returning_false_halts_callback_if_config_variable_is_true + obj = CallbackFalseTerminator.new + assert_deprecated do + obj.save + assert_equal :second, obj.halted + assert !obj.saved + end + end + end + + class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase + def setup + ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = false + end + + def test_returning_false_does_not_halt_callback_if_config_variable_is_false + obj = CallbackFalseTerminator.new + obj.save + assert_equal nil, obj.halted + assert obj.saved + end + end + class HyphenatedKeyTest < ActiveSupport::TestCase def test_save obj = HyphenatedCallbacks.new diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 60bd8a06aa..253c1adc23 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -59,24 +59,24 @@ class ConcernTest < ActiveSupport::TestCase 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_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 +84,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/big_decimal/yaml_conversions_test.rb b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb deleted file mode 100644 index e634679d20..0000000000 --- a/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'abstract_unit' - -class BigDecimalYamlConversionsTest < ActiveSupport::TestCase - def test_to_yaml - assert_deprecated { require 'active_support/core_ext/big_decimal/yaml_conversions' } - assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml) - assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml) - assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml) - assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml) - 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/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index a89202f518..c283b546e6 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -30,7 +30,6 @@ class DurationTest < ActiveSupport::TestCase assert ActiveSupport::Duration === 1.day assert !(ActiveSupport::Duration === 1.day.to_i) assert !(ActiveSupport::Duration === 'foo') - assert !(ActiveSupport::Duration === ActiveSupport::ProxyObject.new) end def test_equals @@ -40,14 +39,22 @@ class DurationTest < ActiveSupport::TestCase assert !(1.day == 'foo') end + def test_to_s + assert_equal "1", 1.second.to_s + end + def test_eql rubinius_skip "Rubinius' #eql? definition relies on #instance_of? " \ "which behaves oddly for the sake of backward-compatibility." assert 1.minute.eql?(1.minute) + assert 1.minute.eql?(60.seconds) assert 2.days.eql?(48.hours) assert !1.second.eql?(1) assert !1.eql?(1.second) + assert 1.minute.eql?(180.seconds - 2.minutes) + assert !1.minute.eql?(60) + assert !1.minute.eql?('foo') end def test_inspect @@ -63,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 @@ -90,8 +106,8 @@ class DurationTest < ActiveSupport::TestCase def test_since_and_ago t = Time.local(2000) - assert t + 1, 1.second.since(t) - assert t - 1, 1.second.ago(t) + assert_equal t + 1, 1.second.since(t) + assert_equal t - 1, 1.second.ago(t) end def test_since_and_ago_without_argument @@ -183,4 +199,25 @@ class DurationTest < ActiveSupport::TestCase cased = case 1.day when 1.day then "ok" end assert_equal cased, "ok" end + + def test_respond_to + assert_respond_to 1.day, :since + assert_respond_to 1.day, :zero? + end + + def test_hash + assert_equal 1.minute.hash, 60.seconds.hash + end + + def test_comparable + assert_equal(-1, (0.seconds <=> 1.second)) + assert_equal(-1, (1.second <=> 1.minute)) + assert_equal(-1, (1 <=> 1.minute)) + assert_equal(0, (0.seconds <=> 0.seconds)) + assert_equal(0, (0.seconds <=> 0.minutes)) + assert_equal(0, (1.second <=> 1.second)) + assert_equal(1, (1.second <=> 0.second)) + assert_equal(1, (1.minute <=> 1.second)) + assert_equal(1, (61 <=> 1.minute)) + end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 6fcf6e8743..346dc3d208 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -71,14 +71,14 @@ class EnumerableTests < ActiveSupport::TestCase def test_index_by payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, - payments.index_by { |p| p.price }) + payments.index_by(&:price)) assert_equal Enumerator, payments.index_by.class if Enumerator.method_defined? :size assert_equal nil, payments.index_by.size assert_equal 42, (1..42).index_by.size end assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, - payments.index_by.each { |p| p.price }) + payments.index_by.each(&:price)) end def test_many 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 5e9fdfd872..e10bee5e00 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -365,7 +365,7 @@ class HashExtTest < ActiveSupport::TestCase :member? => true } hashes.each do |name, hash| - method_map.sort_by { |m| m.to_s }.each do |meth, expected| + method_map.sort_by(&:to_s).each do |meth, expected| assert_equal(expected, hash.__send__(meth, 'a'), "Calling #{name}.#{meth} 'a'") assert_equal(expected, hash.__send__(meth, :a), @@ -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/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index a87af0007c..503e6595cb 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -15,7 +15,6 @@ class KernelTest < ActiveSupport::TestCase assert_equal old_verbose, $VERBOSE end - def test_enable_warnings enable_warnings { assert_equal true, $VERBOSE } assert_equal 1234, enable_warnings { 1234 } @@ -29,57 +28,11 @@ class KernelTest < ActiveSupport::TestCase assert_equal old_verbose, $VERBOSE end - - def test_silence_stream - old_stream_position = STDOUT.tell - silence_stream(STDOUT) { STDOUT.puts 'hello world' } - assert_equal old_stream_position, STDOUT.tell - rescue Errno::ESPIPE - # Skip if we can't stream.tell - end - - def test_silence_stream_closes_file_descriptors - stream = StringIO.new - dup_stream = StringIO.new - stream.stubs(:dup).returns(dup_stream) - dup_stream.expects(:close) - silence_stream(stream) { stream.puts 'hello world' } - end - - def test_quietly - old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell - assert_deprecated do - quietly do - puts 'see me, feel me' - STDERR.puts 'touch me, heal me' - end - end - assert_equal old_stdout_position, STDOUT.tell - assert_equal old_stderr_position, STDERR.tell - rescue Errno::ESPIPE - # Skip if we can't STDERR.tell - end - def test_class_eval o = Object.new class << o; @x = 1; end assert_equal 1, o.class_eval { @x } end - - def test_capture - assert_deprecated do - assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } - end - assert_deprecated do - assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } - end - assert_deprecated do - assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } - end - assert_deprecated do - assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } - end - end end class KernelSuppressTest < ActiveSupport::TestCase @@ -112,27 +65,3 @@ class MockStdErr puts(message) end end - -class KernelDebuggerTest < ActiveSupport::TestCase - def test_debugger_not_available_message_to_stderr - old_stderr = $stderr - $stderr = MockStdErr.new - debugger - assert_match(/Debugger requested/, $stderr.output.first) - ensure - $stderr = old_stderr - end - - def test_debugger_not_available_message_to_rails_logger - rails = Class.new do - def self.logger - @logger ||= MockStdErr.new - end - end - Object.const_set(:Rails, rails) - debugger - assert_match(/Debugger requested/, rails.logger.output.first) - ensure - Object.send(:remove_const, :Rails) - end -end if RUBY_VERSION < '2.0.0' diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index 5f804c749b..b2a75a2bcc 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -1,26 +1,11 @@ require 'abstract_unit' require 'active_support/core_ext/load_error' -class TestMissingSourceFile < ActiveSupport::TestCase - def test_with_require - assert_raise(MissingSourceFile) { require 'no_this_file_don\'t_exist' } - end - def test_with_load - assert_raise(MissingSourceFile) { load 'nor_does_this_one' } - end - def test_path - begin load 'nor/this/one.rb' - rescue MissingSourceFile => e - assert_equal 'nor/this/one.rb', e.path - end - end - def test_is_missing - begin load 'nor_does_this_one' - rescue MissingSourceFile => e - assert e.is_missing?('nor_does_this_one') - assert e.is_missing?('nor_does_this_one.rb') - assert_not e.is_missing?('some_other_file') +class TestMissingSourceFile < ActiveSupport::TestCase + def test_it_is_deprecated + assert_deprecated do + MissingSourceFile.new end end end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 380f5ad42b..c9c9b66a6c 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -56,8 +56,14 @@ Developer = Struct.new(:client) do delegate :name, :to => :client, :prefix => nil end +Event = Struct.new(:case) do + delegate :foo, :to => :case +end + Tester = Struct.new(:client) do delegate :name, :to => :client, :prefix => false + + def foo; 1; end end Product = Struct.new(:name) do @@ -72,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 @@ -495,4 +501,9 @@ class MethodAliasingTest < ActiveSupport::TestCase assert_equal 'duck_with_orange', @instance.duck assert FooClassWithBarMethod.public_method_defined?(:duck) end + + def test_delegate_with_case + event = Event.new(Tester.new) + assert_equal 1, event.foo + 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/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index 34679cb362..d37f4bd0d8 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -4,18 +4,9 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/numeric/time' class DuplicableTest < ActiveSupport::TestCase - RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, 5.seconds, method(:puts)] + RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts)] ALLOW_DUP = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] - - # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead - # raises TypeError exception. Checking here on the runtime whether BigDecimal - # will allow dup or not. - begin - bd = BigDecimal.new('4.56') - ALLOW_DUP << bd.dup - rescue TypeError - RAISE_DUP << bd - end + ALLOW_DUP << BigDecimal.new('4.56') def test_duplicable RAISE_DUP.each do |v| diff --git a/activesupport/test/core_ext/object/itself_test.rb b/activesupport/test/core_ext/object/itself_test.rb deleted file mode 100644 index 65db0ddf40..0000000000 --- a/activesupport/test/core_ext/object/itself_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/object' - -class Object::ItselfTest < ActiveSupport::TestCase - test 'itself returns self' do - object = 'fun' - assert_equal object, object.itself - end -end diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index 225c20fa36..89438675c1 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -30,10 +30,6 @@ class ObjectTryTest < ActiveSupport::TestCase assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') } end - def test_try_only_block_bang - assert_equal @string.reverse, @string.try! { |s| s.reverse } - end - def test_valid_method assert_equal 5, @string.try(:size) end @@ -56,7 +52,11 @@ class ObjectTryTest < ActiveSupport::TestCase end def test_try_only_block - assert_equal @string.reverse, @string.try { |s| s.reverse } + assert_equal @string.reverse, @string.try(&:reverse) + end + + def test_try_only_block_bang + assert_equal @string.reverse, @string.try!(&:reverse) end def test_try_only_block_nil @@ -69,6 +69,10 @@ class ObjectTryTest < ActiveSupport::TestCase assert_equal @string.reverse, @string.try { reverse } end + def test_try_with_instance_eval_block_bang + assert_equal @string.reverse, @string.try! { reverse } + end + def test_try_with_private_method_bang klass = Class.new do private diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 98c4ec6b5e..f096328cee 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -115,11 +115,11 @@ class RangeTest < ActiveSupport::TestCase def test_date_time_with_each datetime = DateTime.now - assert ((datetime - 1.hour)..datetime).each {} + assert(((datetime - 1.hour)..datetime).each {}) end def test_date_time_with_step datetime = DateTime.now - assert ((datetime - 1.hour)..datetime).step(1) {} + assert(((datetime - 1.hour)..datetime).step(1) {}) end end diff --git a/activesupport/test/core_ext/securerandom.rb b/activesupport/test/core_ext/securerandom.rb new file mode 100644 index 0000000000..dfacb7fe9f --- /dev/null +++ b/activesupport/test/core_ext/securerandom.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 18b4727c3b..cb24147ab3 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' @@ -189,10 +188,10 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_squish - original = %{\u180E\u180E A string surrounded by unicode mongolian vowel separators, - with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u180E\u180E} + original = %{\u205f\u3000 A string surrounded by various unicode spaces, + with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} - expected = "A string surrounded by unicode mongolian vowel separators, " + + expected = "A string surrounded by various unicode spaces, " + "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." # Make sure squish returns what we expect: @@ -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) @@ -260,20 +268,32 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_remove - assert_equal "Summer", "Fast Summer".remove(/Fast /) - assert_equal "Summer", "Fast Summer".remove!(/Fast /) + original = "This is a good day to die" + assert_equal "This is a good day", original.remove(" to die") + assert_equal "This is a good day", original.remove(" to ", /die/) + assert_equal "This is a good day to die", original + end + + def test_remove_for_multiple_occurrences + original = "This is a good day to die to die" + assert_equal "This is a good day", original.remove(" to die") + assert_equal "This is a good day to die to die", original + end + + def test_remove! + original = "This is a very good day to die" + assert_equal "This is a good day to die", original.remove!(" very") + assert_equal "This is a good day to die", original + assert_equal "This is a good day", original.remove!(" to ", /die/) + assert_equal "This is a good day", original end def test_constantize - run_constantize_tests_on do |string| - string.constantize - end + run_constantize_tests_on(&:constantize) end def test_safe_constantize - run_safe_constantize_tests_on do |string| - string.safe_constantize - end + run_safe_constantize_tests_on(&:safe_constantize) end end @@ -404,11 +424,11 @@ class StringConversionsTest < ActiveSupport::TestCase end def test_partial_string_to_time - with_env_tz "Europe/Moscow" do + with_env_tz "Europe/Moscow" do # use timezone which does not observe DST. now = Time.now assert_equal Time.local(now.year, now.month, now.day, 23, 50), "23:50".to_time assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "23:50".to_time(:utc) - assert_equal Time.local(now.year, now.month, now.day, 18, 50), "13:50 -0100".to_time + assert_equal Time.local(now.year, now.month, now.day, 17, 50), "13:50 -0100".to_time assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "22:50 -0100".to_time(:utc) end end @@ -655,16 +675,6 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal other, "<foo>other" end - test "Deprecated #prepend! method is still present" do - other = "other".html_safe - - assert_deprecated do - other.prepend! "<foo>" - end - - assert_equal other, "<foo>other" - end - test "Concatting safe onto unsafe yields unsafe" do @other_string = "other" diff --git a/activesupport/test/core_ext/struct_test.rb b/activesupport/test/core_ext/struct_test.rb deleted file mode 100644 index 0dff7b32d2..0000000000 --- a/activesupport/test/core_ext/struct_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/struct' - -class StructExt < ActiveSupport::TestCase - def test_to_h - x = Struct.new(:foo, :bar) - z = x.new(1, 2) - assert_equal({ foo: 1, bar: 2 }, z.to_h) - end -end diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb deleted file mode 100644 index 6a7c6e0604..0000000000 --- a/activesupport/test/core_ext/thread_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/thread' - -class ThreadExt < ActiveSupport::TestCase - def test_main_thread_variable_in_enumerator - assert_equal Thread.main, Thread.current - - Thread.current.thread_variable_set :foo, "bar" - - thread, value = Fiber.new { - Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)] - }.resume - - assert_equal Thread.current, thread - assert_equal Thread.current.thread_variable_get(:foo), value - end - - def test_thread_variable_in_enumerator - Thread.new { - Thread.current.thread_variable_set :foo, "bar" - - thread, value = Fiber.new { - Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)] - }.resume - - assert_equal Thread.current, thread - assert_equal Thread.current.thread_variable_get(:foo), value - }.join - end - - def test_thread_variables - assert_equal [], Thread.new { Thread.current.thread_variables }.join.value - - t = Thread.new { - Thread.current.thread_variable_set(:foo, "bar") - Thread.current.thread_variables - } - assert_equal [:foo], t.join.value - end - - def test_thread_variable? - assert_not Thread.new { Thread.current.thread_variable?("foo") }.join.value - t = Thread.new { - Thread.current.thread_variable_set("foo", "bar") - }.join - - assert t.thread_variable?("foo") - assert t.thread_variable?(:foo) - assert_not t.thread_variable?(:bar) - end - - def test_thread_variable_strings_and_symbols_are_the_same_key - t = Thread.new {}.join - t.thread_variable_set("foo", "bar") - assert_equal "bar", t.thread_variable_get(:foo) - end - - def test_thread_variable_frozen - t = Thread.new { }.join - t.freeze - assert_raises(RuntimeError) do - t.thread_variable_set(:foo, "bar") - end - end - - def test_thread_variable_frozen_after_set - t = Thread.new { }.join - t.thread_variable_set :foo, "bar" - t.freeze - assert_raises(RuntimeError) do - t.thread_variable_set(:baz, "qux") - end - end - -end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 9a5bd19be2..d59775001b 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -387,6 +387,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:min => 6) assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(:sec => 7) assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8) + assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000) + assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) } end def test_utc_change @@ -396,6 +398,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(:hour => 16) assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(:hour => 16, :min => 45) assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(:min => 45) + assert_equal Time.utc(2005,1,2,11,22,33,8), Time.utc(2005,1,2,11,22,33,2).change(:nsec => 8000) end def test_offset_change @@ -405,7 +408,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.new(2005,2,22,16,0,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16) assert_equal Time.new(2005,2,22,16,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16, :min => 45) assert_equal Time.new(2005,2,22,15,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:min => 45) + assert_equal Time.new(2005,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,0,"-08:00").change(:sec => 10) + assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:usec => 10).usec + assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:nsec => 10).nsec assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:usec => 1000000) } + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:nsec => 1000000000) } end def test_advance diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 3000da8da4..92c233d567 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -79,6 +79,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal '1999-12-31 19:00:00 EST -0500', @twz.strftime('%Y-%m-%d %H:%M:%S %Z %z') end + def test_strftime_with_escaping + assert_equal '%Z %z', @twz.strftime('%%Z %%z') + assert_equal '%EST %-0500', @twz.strftime('%%%Z %%%z') + end + def test_inspect assert_equal 'Fri, 31 Dec 1999 19:00:00 EST -05:00', @twz.inspect end @@ -807,6 +812,8 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_no_method_error_has_proper_context + rubinius_skip "Error message inconsistency" + e = assert_raises(NoMethodError) { @twz.this_method_does_not_exist } 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 899bb75eae..702e26859a 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -16,15 +16,18 @@ module ModuleWithConstant end class DependenciesTest < ActiveSupport::TestCase - def teardown - ActiveSupport::Dependencies.clear + include DependenciesTestHelpers + + setup do + @loaded_features_copy = $LOADED_FEATURES.dup end - include DependenciesTestHelpers + teardown do + ActiveSupport::Dependencies.clear + $LOADED_FEATURES.replace(@loaded_features_copy) + end def test_depend_on_path - skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0' - expected = assert_raises(LoadError) do Kernel.require 'omgwtfbbq' end @@ -53,8 +56,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 2, ActiveSupport::Dependencies.loaded.size end ensure - Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) - Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo) + remove_constants(:ServiceOne, :ServiceTwo) end def test_tracking_identical_loaded_files @@ -64,11 +66,11 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 1, ActiveSupport::Dependencies.loaded.size end ensure - Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) + remove_constants(:ServiceOne) end def test_missing_dependency_raises_missing_source_file - assert_raise(MissingSourceFile) { require_dependency("missing_service") } + assert_raise(LoadError) { require_dependency("missing_service") } end def test_dependency_which_raises_exception_isnt_added_to_loaded_set @@ -84,8 +86,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 'Loading me failed, so do not add to loaded or history.', e.message assert_equal count + 1, $raises_exception_load_count - assert !ActiveSupport::Dependencies.loaded.include?(filename) - assert !ActiveSupport::Dependencies.history.include?(filename) + assert_not ActiveSupport::Dependencies.loaded.include?(filename) + assert_not ActiveSupport::Dependencies.history.include?(filename) end end end @@ -93,7 +95,6 @@ class DependenciesTest < ActiveSupport::TestCase def test_dependency_which_raises_doesnt_blindly_call_blame_file! with_loading do filename = 'dependencies/raises_exception_without_blame_file' - assert_raises(Exception) { require_dependency filename } end end @@ -101,13 +102,12 @@ class DependenciesTest < ActiveSupport::TestCase def test_warnings_should_be_enabled_on_first_load with_loading 'dependencies' do old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true - filename = "check_warnings" expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}") $check_warnings_load_count = 0 - assert !ActiveSupport::Dependencies.loaded.include?(expanded) - assert !ActiveSupport::Dependencies.history.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.history.include?(expanded) silence_warnings { require_dependency filename } assert_equal 1, $check_warnings_load_count @@ -115,7 +115,7 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.loaded.include?(expanded) ActiveSupport::Dependencies.clear - assert !ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) assert ActiveSupport::Dependencies.history.include?(expanded) silence_warnings { require_dependency filename } @@ -124,7 +124,7 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.loaded.include?(expanded) ActiveSupport::Dependencies.clear - assert !ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) assert ActiveSupport::Dependencies.history.include?(expanded) enable_warnings { require_dependency filename } @@ -157,6 +157,31 @@ class DependenciesTest < ActiveSupport::TestCase end end + 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 + end + end + + def test_require_dependency_does_not_assume_any_particular_constant_is_defined + with_autoloading_fixtures do + require_dependency 'typo' + assert_equal 1, TypO + end + end + + # Regression, see https://github.com/rails/rails/issues/16468. + def test_require_dependency_interaction_with_autoloading + with_autoloading_fixtures do + require_dependency 'typo' + 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 + end + end + def test_module_loading with_autoloading_fixtures do assert_kind_of Module, A @@ -178,43 +203,49 @@ class DependenciesTest < ActiveSupport::TestCase def test_directories_manifest_as_modules_unless_const_defined with_autoloading_fixtures do assert_kind_of Module, ModuleFolder - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_module_with_nested_class with_autoloading_fixtures do assert_kind_of Class, ModuleFolder::NestedClass - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_module_with_nested_inline_class with_autoloading_fixtures do assert_kind_of Class, ModuleFolder::InlineClass - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_directories_may_manifest_as_nested_classes with_autoloading_fixtures do assert_kind_of Class, ClassFolder - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_class with_autoloading_fixtures do assert_kind_of Class, ClassFolder::NestedClass - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_inline_class with_autoloading_fixtures do assert_kind_of Class, ClassFolder::InlineClass - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_inline_subclass_of_parent @@ -222,8 +253,9 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Class, ClassFolder::ClassFolderSubclass assert_kind_of Class, ClassFolder assert_equal 'indeed', ClassFolder::ClassFolderSubclass::ConstantInClassFolder - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_nested_class_can_access_sibling @@ -231,16 +263,15 @@ class DependenciesTest < ActiveSupport::TestCase sibling = ModuleFolder::NestedClass.class_eval "NestedSibling" assert defined?(ModuleFolder::NestedSibling) assert_equal ModuleFolder::NestedSibling, sibling - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_doesnt_break_normal_require path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) - with_autoloading_fixtures do # The _ = assignments are to prevent warnings _ = RequiresConstant @@ -252,15 +283,13 @@ class DependenciesTest < ActiveSupport::TestCase assert defined?(LoadedConstant) end ensure - remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) - $".replace(original_features) + remove_constants(:RequiresConstant, :LoadedConstant) $:.replace(original_path) end def test_doesnt_break_normal_require_nested path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_autoloading_fixtures do @@ -275,14 +304,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_true_when_file_not_yet_required path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -290,14 +317,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -306,14 +331,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_false_when_file_already_required path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -322,7 +345,6 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end @@ -330,14 +352,11 @@ class DependenciesTest < ActiveSupport::TestCase with_loading do assert_raise(LoadError) { require 'this_file_dont_exist_dude' } end - ensure - remove_constants(:LoadedConstant) end def test_load_returns_true_when_file_found path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -346,7 +365,6 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end @@ -354,17 +372,16 @@ class DependenciesTest < ActiveSupport::TestCase with_loading do assert_raise(LoadError) { load 'this_file_dont_exist_dude.rb' } end - ensure - remove_constants(:LoadedConstant) end def failing_test_access_thru_and_upwards_fails with_autoloading_fixtures do - assert ! defined?(ModuleFolder) + assert_not defined?(ModuleFolder) assert_raise(NameError) { ModuleFolder::Object } assert_raise(NameError) { ModuleFolder::NestedClass::Object } - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_non_existing_const_raises_name_error_with_fully_qualified_name @@ -377,6 +394,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal "uninitialized constant A::B::DoesNotExist", e.message assert_equal :DoesNotExist, e.name end + ensure + remove_constants(:A) end def test_smart_name_error_strings @@ -466,9 +485,9 @@ class DependenciesTest < ActiveSupport::TestCase nil_name = Module.new def nil_name.name() nil end assert !ActiveSupport::Dependencies.autoloaded?(nil_name) - - Object.class_eval { remove_const :ModuleFolder } end + ensure + remove_constants(:ModuleFolder) end def test_qualified_name_for @@ -526,6 +545,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Module, ::ModuleWithCustomConstMissing::A assert_kind_of String, ::ModuleWithCustomConstMissing::A::B end + ensure + remove_constants(:ModuleWithCustomConstMissing) end def test_const_missing_in_anonymous_modules_loads_top_level_constants @@ -534,6 +555,8 @@ class DependenciesTest < ActiveSupport::TestCase klass = Class.new.class_eval "E" assert_equal E, klass end + ensure + remove_constants(:E) end def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object @@ -545,18 +568,22 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message assert_equal :E, e.name end + ensure + remove_constants(:E) end def test_removal_from_tree_should_be_detected with_loading 'dependencies' do c = ServiceOne ActiveSupport::Dependencies.clear - assert ! defined?(ServiceOne) + assert_not defined?(ServiceOne) e = assert_raise ArgumentError do ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing) end assert_match %r{ServiceOne has been removed from the module tree}i, e.message end + ensure + remove_constants(:ServiceOne) end def test_references_should_work @@ -565,42 +592,46 @@ class DependenciesTest < ActiveSupport::TestCase service_one_first = ServiceOne assert_equal service_one_first, c.get("ServiceOne") ActiveSupport::Dependencies.clear - assert ! defined?(ServiceOne) - + assert_not defined?(ServiceOne) service_one_second = ServiceOne assert_not_equal service_one_first, c.get("ServiceOne") assert_equal service_one_second, c.get("ServiceOne") end + ensure + remove_constants(:ServiceOne) end def test_constantize_shortcut_for_cached_constant_lookups with_loading 'dependencies' do assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne") end + ensure + remove_constants(:ServiceOne) end def test_nested_load_error_isnt_rescued with_loading 'dependencies' do - assert_raise(MissingSourceFile) do + assert_raise(LoadError) do RequiresNonexistent1 end end end def test_autoload_once_paths_do_not_add_to_autoloaded_constants + old_path = ActiveSupport::Dependencies.autoload_once_paths with_autoloading_fixtures do ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths.dup - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder) + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder) 1 if ModuleFolder::NestedClass # 1 if to avoid warning - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) end ensure - Object.class_eval { remove_const :ModuleFolder } - ActiveSupport::Dependencies.autoload_once_paths = [] + remove_constants(:ModuleFolder) + ActiveSupport::Dependencies.autoload_once_paths = old_path end def test_autoload_once_pathnames_do_not_add_to_autoloaded_constants @@ -609,15 +640,15 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.autoload_paths = pathnames ActiveSupport::Dependencies.autoload_once_paths = pathnames - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder) + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder) 1 if ModuleFolder::NestedClass # 1 if to avoid warning - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) end ensure - Object.class_eval { remove_const :ModuleFolder } + remove_constants(:ModuleFolder) ActiveSupport::Dependencies.autoload_once_paths = [] end @@ -627,6 +658,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 10, ApplicationController assert ActiveSupport::Dependencies.autoloaded?(:ApplicationController) end + ensure + remove_constants(:ApplicationController) end def test_preexisting_constants_are_not_marked_as_autoloaded @@ -642,9 +675,8 @@ class DependenciesTest < ActiveSupport::TestCase assert ! ActiveSupport::Dependencies.autoloaded?(:E), "E shouldn't be marked autoloaded!" ActiveSupport::Dependencies.clear end - ensure - Object.class_eval { remove_const :E } + remove_constants(:E) end def test_constants_in_capitalized_nesting_marked_as_autoloaded @@ -653,6 +685,8 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.autoloaded?("HTML::SomeClass") end + ensure + remove_constants(:HTML) end def test_unloadable @@ -683,7 +717,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal false, M.unloadable end ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_unloadable_constants_should_receive_callback @@ -694,7 +728,7 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear assert !defined?(C) ensure - Object.class_eval { remove_const :C } if defined?(C) + remove_constants(:C) end def test_new_contants_in_without_constants @@ -708,7 +742,7 @@ class DependenciesTest < ActiveSupport::TestCase }.map(&:to_s) assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :Hello } + remove_constants(:Hello) end def test_new_constants_in_with_nesting @@ -725,9 +759,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["OuterAfter", "OuterBefore"], outer.sort.map(&:to_s) assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - %w(OuterBefore Inner OuterAfter).each do |name| - Object.class_eval { remove_const name if const_defined?(name) } - end + remove_constants(:OuterBefore, :Inner, :OuterAfter) end def test_new_constants_in_module @@ -746,7 +778,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_new_constants_in_module_using_name @@ -764,7 +796,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_new_constants_in_with_inherited_constants @@ -782,26 +814,27 @@ class DependenciesTest < ActiveSupport::TestCase def test_file_with_multiple_constants_and_require_dependency with_autoloading_fixtures do - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) require_dependency 'multiple_constant_file' assert defined?(MultipleConstantFile) assert defined?(SiblingConstant) assert ActiveSupport::Dependencies.autoloaded?(:MultipleConstantFile) assert ActiveSupport::Dependencies.autoloaded?(:SiblingConstant) - ActiveSupport::Dependencies.clear - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) end + ensure + remove_constants(:MultipleConstantFile, :SiblingConstant) end def test_file_with_multiple_constants_and_auto_loading with_autoloading_fixtures do - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) assert_equal 10, MultipleConstantFile @@ -812,15 +845,17 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) end + ensure + remove_constants(:MultipleConstantFile, :SiblingConstant) end def test_nested_file_with_multiple_constants_and_require_dependency with_autoloading_fixtures do - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) require_dependency 'class_folder/nested_class' @@ -831,15 +866,17 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) end + ensure + remove_constants(:ClassFolder) end def test_nested_file_with_multiple_constants_and_auto_loading with_autoloading_fixtures do - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) assert_kind_of Class, ClassFolder::NestedClass @@ -850,9 +887,11 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) end + ensure + remove_constants(:ClassFolder) end def test_autoload_doesnt_shadow_no_method_error_with_relative_constant @@ -863,9 +902,8 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNoMethodError) end def test_autoload_doesnt_shadow_no_method_error_with_absolute_constant @@ -876,9 +914,8 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNoMethodError) end def test_autoload_doesnt_shadow_error_when_mechanism_not_set_to_load @@ -894,7 +931,6 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoload_doesnt_shadow_name_error with_autoloading_fixtures do - Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) 2.times do e = assert_raise NameError do ::RaisesNameError::FooBarBaz.object_id @@ -909,19 +945,20 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNameError) end def test_remove_constant_handles_double_colon_at_start Object.const_set 'DeleteMe', Module.new DeleteMe.const_set 'OrMe', Module.new ActiveSupport::Dependencies.remove_constant "::DeleteMe::OrMe" - assert ! defined?(DeleteMe::OrMe) + assert_not defined?(DeleteMe::OrMe) assert defined?(DeleteMe) ActiveSupport::Dependencies.remove_constant "::DeleteMe" - assert ! defined?(DeleteMe) + assert_not defined?(DeleteMe) + ensure + remove_constants(:DeleteMe) end def test_remove_constant_does_not_trigger_loading_autoloads @@ -931,7 +968,9 @@ class DependenciesTest < ActiveSupport::TestCase end assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant" - assert !defined?(ShouldNotBeAutoloaded) + assert_not defined?(ShouldNotBeAutoloaded) + ensure + remove_constants(constant) end def test_remove_constant_does_not_autoload_already_removed_parents_as_a_side_effect @@ -940,11 +979,14 @@ class DependenciesTest < ActiveSupport::TestCase _ = ::A::B # assignment to silence parse-time warning "possibly useless use of :: in void context" ActiveSupport::Dependencies.remove_constant('A') ActiveSupport::Dependencies.remove_constant('A::B') - assert !defined?(A) + assert_not defined?(A) end + ensure + remove_constants(:A) end def test_load_once_constants_should_not_be_unloaded + old_path = ActiveSupport::Dependencies.autoload_once_paths with_autoloading_fixtures do ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context" @@ -953,8 +995,8 @@ class DependenciesTest < ActiveSupport::TestCase assert defined?(A) end ensure - ActiveSupport::Dependencies.autoload_once_paths = [] - Object.class_eval { remove_const :A if const_defined?(:A) } + ActiveSupport::Dependencies.autoload_once_paths = old_path + remove_constants(:A) end def test_access_unloaded_constants_for_reload @@ -966,29 +1008,45 @@ class DependenciesTest < ActiveSupport::TestCase A::B # Make sure no circular dependency error end + ensure + remove_constants(:A) end def test_autoload_once_paths_should_behave_when_recursively_loading + old_path = ActiveSupport::Dependencies.autoload_once_paths with_loading 'dependencies', 'autoloading_fixtures' do ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last] - assert !defined?(CrossSiteDependency) + assert_not defined?(CrossSiteDependency) assert_nothing_raised { CrossSiteDepender.nil? } assert defined?(CrossSiteDependency) - assert !ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency), + assert_not ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency), "CrossSiteDependency shouldn't be marked as autoloaded!" ActiveSupport::Dependencies.clear assert defined?(CrossSiteDependency), "CrossSiteDependency shouldn't have been unloaded!" end ensure - ActiveSupport::Dependencies.autoload_once_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = old_path + remove_constants(:CrossSiteDependency) end def test_hook_called_multiple_times assert_nothing_raised { ActiveSupport::Dependencies.hook! } end + def test_load_and_require_stay_private + assert Object.private_methods.include?(:load) + assert Object.private_methods.include?(:require) + + ActiveSupport::Dependencies.unhook! + + assert Object.private_methods.include?(:load) + assert Object.private_methods.include?(:require) + ensure + ActiveSupport::Dependencies.hook! + end + def test_unhook ActiveSupport::Dependencies.unhook! assert !Module.new.respond_to?(:const_missing_without_dependencies) diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 9268512a97..e4d5197112 100644 --- a/activesupport/test/dependencies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -13,6 +13,7 @@ module DependenciesTestHelpers ActiveSupport::Dependencies.autoload_paths = prior_autoload_paths ActiveSupport::Dependencies.mechanism = old_mechanism ActiveSupport::Dependencies.explicitly_unloadable_constants = [] + ActiveSupport::Dependencies.clear end def with_autoloading_fixtures(&block) 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 new file mode 100644 index 0000000000..1facd691fa --- /dev/null +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -0,0 +1,11 @@ +require 'abstract_unit' +require 'active_support/hash_with_indifferent_access' + +class HashWithIndifferentAccessTest < ActiveSupport::TestCase + def test_reverse_merge + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + end + +end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index b37f31bc5f..be68bb2e2e 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -125,6 +125,9 @@ class InflectorTest < ActiveSupport::TestCase ["PhDRequired", "phd_required", "PhD required", "PhD Required"], ["IRoRU", "i_ror_u", "I RoR u", "I RoR U"], ["RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"], + ["HTTP::RESTful", "http/restful", "HTTP/RESTful", "HTTP/RESTful"], + ["HTTP::RESTfulAPI", "http/restful_api", "HTTP/RESTful API", "HTTP/RESTful API"], + ["APIRESTful", "api_restful", "API RESTful", "API RESTful"], # misdirection ["Capistrano", "capistrano", "Capistrano", "Capistrano"], @@ -487,8 +490,8 @@ class InflectorTest < ActiveSupport::TestCase assert_equal [], inflect.uncountables # restore all the inflections - singulars.reverse.each { |singular| inflect.singular(*singular) } - plurals.reverse.each { |plural| inflect.plural(*plural) } + singulars.reverse_each { |singular| inflect.singular(*singular) } + plurals.reverse_each { |plural| inflect.plural(*plural) } inflect.uncountable(uncountables) assert_equal singulars, inflect.singulars 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 ad358ad21d..63d921e3b4 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' @@ -68,7 +67,7 @@ class TestJSONEncoding < ActiveSupport::TestCase [ 1.0/0.0, %(null) ], [ -1.0/0.0, %(null) ], [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], - [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]] + [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], @@ -176,46 +175,6 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal "𐒑", decoded_hash['string'] end - def test_reading_encode_big_decimal_as_string_option - assert_deprecated do - assert ActiveSupport.encode_big_decimal_as_string - end - end - - def test_setting_deprecated_encode_big_decimal_as_string_option - assert_raise(NotImplementedError) do - ActiveSupport.encode_big_decimal_as_string = true - end - - assert_raise(NotImplementedError) do - ActiveSupport.encode_big_decimal_as_string = false - end - end - - def test_exception_raised_when_encoding_circular_reference_in_array - a = [1] - a << a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - - def test_exception_raised_when_encoding_circular_reference_in_hash - a = { :name => 'foo' } - a[:next] = a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - - def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array - a = { :name => 'foo', :sub => [] } - a[:sub] << a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - def test_hash_key_identifiers_are_always_quoted values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"} assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values)) diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index b6c0a08b05..eb71369397 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -1,12 +1,5 @@ require 'abstract_unit' - -begin - require 'openssl' - OpenSSL::Digest::SHA1 -rescue LoadError, NameError - $stderr.puts "Skipping MessageEncryptor test: broken OpenSSL install" -else - +require 'openssl' require 'active_support/time' require 'active_support/json' @@ -97,5 +90,3 @@ class MessageEncryptorTest < ActiveSupport::TestCase ::Base64.strict_encode64(bits) end end - -end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 28035bc428..6c3519df9a 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -1,12 +1,5 @@ require 'abstract_unit' - -begin - require 'openssl' - OpenSSL::Digest::SHA1 -rescue LoadError, NameError - $stderr.puts "Skipping MessageVerifier test: broken OpenSSL install" -else - +require 'openssl' require 'active_support/time' require 'active_support/json' @@ -27,21 +20,29 @@ class MessageVerifierTest < ActiveSupport::TestCase @data = { :some => "data", :now => Time.local(2010) } end + def test_valid_message + data, hash = @verifier.generate(@data).split("--") + assert !@verifier.valid_message?(nil) + assert !@verifier.valid_message?("") + assert !@verifier.valid_message?("#{data.reverse}--#{hash}") + assert !@verifier.valid_message?("#{data}--#{hash.reverse}") + assert !@verifier.valid_message?("purejunk") + end + def test_simple_round_tripping message = @verifier.generate(@data) + assert_equal @data, @verifier.verified(message) assert_equal @data, @verifier.verify(message) end - def test_missing_signature_raises - assert_not_verified(nil) - assert_not_verified("") + def test_verified_returns_false_on_invalid_message + assert !@verifier.verified("purejunk") end - def test_tampered_data_raises - data, hash = @verifier.generate(@data).split("--") - assert_not_verified("#{data.reverse}--#{hash}") - assert_not_verified("#{data}--#{hash.reverse}") - assert_not_verified("purejunk") + def test_verify_exception_on_invalid_message + assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do + @verifier.verify("purejunk") + end end def test_alternative_serialization_method @@ -50,6 +51,7 @@ class MessageVerifierTest < ActiveSupport::TestCase verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new) message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) }) exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" } + assert_equal exp, verifier.verified(message) assert_equal exp, verifier.verify(message) ensure ActiveSupport.use_standard_json_time_format = prev @@ -63,6 +65,11 @@ class MessageVerifierTest < ActiveSupport::TestCase # valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" exception = assert_raise(ArgumentError, NameError) do + @verifier.verified(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + exception = assert_raise(ArgumentError, NameError) do @verifier.verify(valid_message) end assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", @@ -75,12 +82,4 @@ class MessageVerifierTest < ActiveSupport::TestCase end assert_equal exception.message, 'Secret should not be nil.' end - - def assert_not_verified(message) - assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do - @verifier.verify(message) - end - end -end - end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index feca013675..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' @@ -182,7 +181,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_sortability - words = %w(builder armor zebra).sort_by { |s| s.mb_chars } + words = %w(builder armor zebra).sort_by(&:mb_chars) assert_equal %w(armor builder zebra), words end diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index aba81b8248..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' @@ -115,7 +114,7 @@ class MultibyteConformanceTest < ActiveSupport::TestCase next if (line.empty? || line =~ /^\#/) cols, comment = line.split("#") - cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? } + cols = cols.split(";").map(&:strip).reject(&:empty?) next unless cols.length == 5 # codepoints are in hex in the test suite, pack wants them as integers 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..23996ef381 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -83,6 +83,10 @@ 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("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/rescuable_test.rb b/activesupport/test/rescuable_test.rb index ec9d231125..bd43ad0797 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -12,6 +12,12 @@ end class CoolError < StandardError end +module WeirdError + def self.===(other) + Exception === other && other.respond_to?(:weird?) + end +end + class Stargate attr_accessor :result @@ -29,6 +35,10 @@ class Stargate @result = e.message end + rescue_from WeirdError do + @result = 'weird' + end + def dispatch(method) send(method) rescue Exception => e @@ -47,6 +57,16 @@ class Stargate raise MadRonon.new("dex") end + def weird + StandardError.new.tap do |exc| + def exc.weird? + true + end + + raise exc + end + end + def sos @result = 'killed' end @@ -91,15 +111,20 @@ class RescuableTest < ActiveSupport::TestCase assert_equal 'dex', @stargate.result end + def test_rescue_from_error_dispatchers_with_case_operator + @stargate.dispatch :weird + assert_equal 'weird', @stargate.result + end + def test_rescues_defined_later_are_added_at_end_of_the_rescue_handlers_array - expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon"] - result = @stargate.send(:rescue_handlers).collect {|e| e.first} + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "WeirdError"] + result = @stargate.send(:rescue_handlers).collect(&:first) assert_equal expected, result end def test_children_should_inherit_rescue_definitions_from_parents_and_child_rescue_should_be_appended - expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "CoolError"] - result = @cool_stargate.send(:rescue_handlers).collect {|e| e.first} + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "WeirdError", "CoolError"] + result = @cool_stargate.send(:rescue_handlers).collect(&:first) assert_equal expected, result end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index efa9d5e61f..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 @@ -165,4 +172,9 @@ class SafeBufferTest < ActiveSupport::TestCase x = 'foo %{x} bar'.html_safe % { x: 'qux' } assert x.html_safe?, 'should be safe' end + + test 'Should not affect frozen objects when accessing characters' do + x = 'Hello'.html_safe + assert_equal x[/a/, 1], nil + end end diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb new file mode 100644 index 0000000000..08d2e3baa6 --- /dev/null +++ b/activesupport/test/security_utils_test.rb @@ -0,0 +1,9 @@ +require 'abstract_unit' +require 'active_support/security_utils' + +class SecurityUtilsTest < ActiveSupport::TestCase + def test_secure_compare_should_perform_string_comparison + assert ActiveSupport::SecurityUtils.secure_compare('a', 'a') + assert !ActiveSupport::SecurityUtils.secure_compare('a', 'b') + end +end diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 27f629474e..03a63e94e8 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -79,6 +79,19 @@ class TaggedLoggingTest < ActiveSupport::TestCase assert_equal "[OMG] Cool story bro\n[BCX] Funky time\n", @output.string end + test "keeps each tag in their own instance" do + @other_output = StringIO.new + @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output)) + @logger.tagged("OMG") do + @other_logger.tagged("BCX") do + @logger.info "Cool story bro" + @other_logger.info "Funky time" + end + end + assert_equal "[OMG] Cool story bro\n", @output.string + assert_equal "[BCX] Funky time\n", @other_output.string + end + test "cleans up the taggings on flush" do @logger.tagged("BCX") do Thread.new do diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 5e852c8050..151b623171 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -182,30 +182,28 @@ class TestOrderTest < ActiveSupport::TestCase ActiveSupport::TestCase.test_order = @original_test_order end - def test_defaults_to_sorted_with_warning + def test_defaults_to_random ActiveSupport::TestCase.test_order = nil - assert_equal :sorted, assert_deprecated { ActiveSupport::TestCase.test_order } + assert_equal :random, ActiveSupport::TestCase.test_order - # It should only produce a deprecation warning the first time this is accessed - assert_equal :sorted, assert_not_deprecated { ActiveSupport::TestCase.test_order } - assert_equal :sorted, assert_not_deprecated { ActiveSupport.test_order } + assert_equal :random, ActiveSupport.test_order end def test_test_order_is_global - ActiveSupport::TestCase.test_order = :random - - assert_equal :random, ActiveSupport.test_order - assert_equal :random, ActiveSupport::TestCase.test_order - assert_equal :random, self.class.test_order - assert_equal :random, Class.new(ActiveSupport::TestCase).test_order - - ActiveSupport.test_order = :sorted + ActiveSupport::TestCase.test_order = :sorted assert_equal :sorted, ActiveSupport.test_order assert_equal :sorted, ActiveSupport::TestCase.test_order assert_equal :sorted, self.class.test_order assert_equal :sorted, Class.new(ActiveSupport::TestCase).test_order + + ActiveSupport.test_order = :random + + assert_equal :random, ActiveSupport.test_order + assert_equal :random, ActiveSupport::TestCase.test_order + 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! 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..676a143692 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -1,5 +1,6 @@ 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 @@ -17,6 +18,7 @@ class TimeTravelTest < ActiveSupport::TestCase assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today + assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end def test_time_helper_travel_with_block @@ -25,10 +27,12 @@ class TimeTravelTest < ActiveSupport::TestCase travel 1.day do assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today + assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_not_equal expected_time.to_date, Date.today + assert_not_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end def test_time_helper_travel_to @@ -37,6 +41,7 @@ class TimeTravelTest < ActiveSupport::TestCase assert_equal expected_time, Time.now assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now end def test_time_helper_travel_to_with_block @@ -45,10 +50,12 @@ class TimeTravelTest < ActiveSupport::TestCase 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 def test_time_helper_travel_back @@ -57,16 +64,20 @@ class TimeTravelTest < ActiveSupport::TestCase travel_to expected_time assert_equal expected_time, Time.now assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now travel_back assert_not_equal expected_time, Time.now assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now end def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do assert_equal 50, Time.now.sec assert_equal 0, Time.now.usec + assert_equal 50, DateTime.now.sec + assert_equal 0, DateTime.now.usec end end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index b7a89ed332..7888b9919b 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -22,7 +22,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000)) end - ActiveSupport::TimeZone::MAPPING.keys.each do |name| + ActiveSupport::TimeZone::MAPPING.each_key do |name| define_method("test_map_#{name.downcase.gsub(/[^a-z]/, '_')}_to_tzinfo") do zone = ActiveSupport::TimeZone[name] assert_respond_to zone.tzinfo, :period_for_local @@ -395,20 +395,14 @@ class TimeZoneTest < ActiveSupport::TestCase assert_raise(ArgumentError) { ActiveSupport::TimeZone[false] } end - def test_unknown_zone_should_have_tzinfo_but_exception_on_utc_offset - zone = ActiveSupport::TimeZone.create("bogus") - assert_instance_of TZInfo::TimezoneProxy, zone.tzinfo - assert_raise(TZInfo::InvalidTimezoneIdentifier) { zone.utc_offset } - end - - def test_unknown_zone_with_utc_offset - zone = ActiveSupport::TimeZone.create("bogus", -21_600) - assert_equal(-21_600, zone.utc_offset) + def test_unknown_zone_raises_exception + assert_raise TZInfo::InvalidTimezoneIdentifier do + ActiveSupport::TimeZone.create("bogus") + end 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 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/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb index 2e962576b5..1314c9065a 100644 --- a/activesupport/test/xml_mini/nokogiri_engine_test.rb +++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb @@ -8,15 +8,13 @@ require 'active_support/xml_mini' require 'active_support/core_ext/hash/conversions' class NokogiriEngineTest < ActiveSupport::TestCase - include ActiveSupport - def setup - @default_backend = XmlMini.backend - XmlMini.backend = 'Nokogiri' + @default_backend = ActiveSupport::XmlMini.backend + ActiveSupport::XmlMini.backend = 'Nokogiri' end def teardown - XmlMini.backend = @default_backend + ActiveSupport::XmlMini.backend = @default_backend end def test_file_from_xml @@ -56,13 +54,13 @@ class NokogiriEngineTest < ActiveSupport::TestCase end def test_setting_nokogiri_as_backend - XmlMini.backend = 'Nokogiri' - assert_equal XmlMini_Nokogiri, XmlMini.backend + ActiveSupport::XmlMini.backend = 'Nokogiri' + assert_equal ActiveSupport::XmlMini_Nokogiri, ActiveSupport::XmlMini.backend end def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse('')) + assert_equal({}, ActiveSupport::XmlMini.parse(nil)) + assert_equal({}, ActiveSupport::XmlMini.parse('')) end def test_array_type_makes_an_array @@ -207,9 +205,9 @@ class NokogiriEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } assert_equal(hash, parsed_xml) end end diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb index 4f078f31e0..7978a50921 100644 --- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb +++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb @@ -8,15 +8,13 @@ require 'active_support/xml_mini' require 'active_support/core_ext/hash/conversions' class NokogiriSAXEngineTest < ActiveSupport::TestCase - include ActiveSupport - def setup - @default_backend = XmlMini.backend - XmlMini.backend = 'NokogiriSAX' + @default_backend = ActiveSupport::XmlMini.backend + ActiveSupport::XmlMini.backend = 'NokogiriSAX' end def teardown - XmlMini.backend = @default_backend + ActiveSupport::XmlMini.backend = @default_backend end def test_file_from_xml @@ -57,13 +55,13 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase end def test_setting_nokogirisax_as_backend - XmlMini.backend = 'NokogiriSAX' - assert_equal XmlMini_NokogiriSAX, XmlMini.backend + ActiveSupport::XmlMini.backend = 'NokogiriSAX' + assert_equal ActiveSupport::XmlMini_NokogiriSAX, ActiveSupport::XmlMini.backend end def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse('')) + assert_equal({}, ActiveSupport::XmlMini.parse(nil)) + assert_equal({}, ActiveSupport::XmlMini.parse('')) end def test_array_type_makes_an_array @@ -208,9 +206,9 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } assert_equal(hash, parsed_xml) end end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index 0c1f11803c..f0067ca656 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -2,19 +2,17 @@ require 'abstract_unit' require 'active_support/xml_mini' class REXMLEngineTest < ActiveSupport::TestCase - include ActiveSupport - def test_default_is_rexml - assert_equal XmlMini_REXML, XmlMini.backend + assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend end def test_set_rexml_as_backend - XmlMini.backend = 'REXML' - assert_equal XmlMini_REXML, XmlMini.backend + ActiveSupport::XmlMini.backend = 'REXML' + assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend end def test_parse_from_io - XmlMini.backend = 'REXML' + ActiveSupport::XmlMini.backend = 'REXML' io = StringIO.new(<<-eoxml) <root> good @@ -29,9 +27,9 @@ class REXMLEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } assert_equal(hash, parsed_xml) end end diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index f49431cbbf..bcd6997b06 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -11,7 +11,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 3ccbd1dad4..b62f90a59e 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -107,6 +107,7 @@ results = {} ENV['GEM'].split(',').each do |gem| [false, true].each do |isolated| + next if ENV['TRAVIS_PULL_REQUEST'] && ENV['TRAVIS_PULL_REQUEST'] != 'false' && isolated next if gem == 'railties' && isolated next if gem == 'aj:integration' && isolated @@ -139,6 +140,6 @@ if failures.empty? else puts puts "Rails build FAILED" - puts "Failed components: #{failures.map { |component| component.first }.join(', ')}" + puts "Failed components: #{failures.map(&:first).join(', ')}" exit(false) end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 2770fc73e7..dd5ca4b395 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,27 +1,13 @@ -* Change Posts to Articles in Getting Started sample application in order to -better align with the actual guides. +* New section in Active Record Association Basics: Single Table Inheritance - *John Kelly Ferguson* + *Andrey Nering* -* Update all Rails 4.1.0 references to 4.1.1 within the guides and code. +* New section in Active Record Querying: Understanding The Method Chaining - *John Kelly Ferguson* + *Andrey Nering* -* Split up rows in the Explain Queries table of the ActiveRecord Querying section -in order to improve readability. +* New section in Configuring: Search Engines Indexing - *John Kelly Ferguson* + *Andrey Nering* -* Change all non-HTTP method 'post' references to 'article'. - - *John Kelly Ferguson* - -* Updates the maintenance policy to match the latest versions of Rails - - *Matias Korhonen* - -* Switched the order of `Applying a default scope` and `Merging of scopes` subsections so default scopes are introduced first. - - *Alex Riabov* - -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/guides/CHANGELOG.md) for previous changes. +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 94d4be8c0a..fd093b94c1 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -34,11 +34,13 @@ namespace :guides do task :help do puts <<-help -Guides are taken from the source directory, and the resulting HTML goes into the +Guides are taken from the source directory, and the result goes into the output directory. Assets are stored under files, and copied to output/files as part of the generation process. -All this process is handled via rake tasks, here's a full list of them: +You can generate HTML, Kindle or both formats using the `guides:generate` task. + +All of these processes are handled via rake tasks, here's a full list of them: #{%x[rake -T]} Some arguments may be passed via environment variables: 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/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index 318a1ef1c7..ed558e4793 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -34,7 +34,7 @@ pre, code { overflow: auto; color: #222; } -pre,tt,code,.note>p { +pre, tt, code { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 9387e3dc1d..032e6bfe11 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -1,14 +1,15 @@ # Activate the gem you are reporting the issue against. -gem 'rails', '4.0.0' +gem 'rails', '4.2.0' require 'rails' +require 'rack/test' require 'action_controller/railtie' class TestApp < Rails::Application config.root = File.dirname(__FILE__) config.session_store :cookie_store, key: 'cookie_store_key' - config.secret_token = 'secret_token' - config.secret_key_base = 'secret_key_base' + secrets.secret_token = 'secret_token' + secrets.secret_key_base = 'secret_key_base' config.logger = Logger.new($stdout) Rails.logger = config.logger @@ -27,7 +28,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 20c64b4a85..9be8130884 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -3,8 +3,6 @@ unless File.exist?('Gemfile') source 'https://rubygems.org' gem 'rails', github: 'rails/rails' gem 'arel', github: 'rails/arel' - gem 'rack', github: 'rack/rack' - gem 'i18n', github: 'svenfuchs/i18n' GEMFILE system 'bundle' @@ -19,8 +17,8 @@ require 'action_controller/railtie' class TestApp < Rails::Application config.root = File.dirname(__FILE__) config.session_store :cookie_store, key: 'cookie_store_key' - config.secret_token = 'secret_token' - config.secret_key_base = 'secret_key_base' + secrets.secret_token = 'secret_token' + secrets.secret_key_base = 'secret_key_base' config.logger = Logger.new($stdout) Rails.logger = config.logger diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index d72633d0b2..b295d9d21f 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -1,5 +1,5 @@ # Activate the gem you are reporting the issue against. -gem 'activerecord', '4.0.0' +gem 'activerecord', '4.2.0' require 'active_record' require 'minitest/autorun' require 'logger' @@ -12,10 +12,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 e7f5d0d5ff..9557f0b7c5 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -3,8 +3,6 @@ unless File.exist?('Gemfile') source 'https://rubygems.org' gem 'rails', github: 'rails/rails' gem 'arel', github: 'rails/arel' - gem 'rack', github: 'rack/rack' - gem 'i18n', github: 'svenfuchs/i18n' gem 'sqlite3' GEMFILE @@ -23,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/rails_guides.rb b/guides/rails_guides.rb index 9d1d5567f6..762ab1c0e2 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -24,11 +24,11 @@ begin require 'redcarpet' rescue LoadError # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Redcarpet 3.1.2+.') + $stderr.puts('Generating guides requires Redcarpet 3.2.2+.') $stderr.puts(<<ERROR) if bundler? Please add - gem 'redcarpet', '~> 3.1.2' + gem 'redcarpet', '~> 3.2.2' to the Gemfile, run diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 09eecd5634..32926622e3 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -70,7 +70,7 @@ module Kindle File.open("sections/%03d/_section.txt" % section_idx, 'w') {|f| f.puts title} doc.xpath("//h3[@id]").each_with_index do |h3,item_idx| subsection = h3.inner_text - content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map {|x| x.to_html} + content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map(&:to_html) item = Nokogiri::HTML(h3.to_html + content.join("\n")) item_path = "sections/%03d/%03d.html" % [section_idx, item_idx] add_head_section(item, subsection) diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 8a908a4339..36183fd321 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -7,11 +7,9 @@ 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 diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 688f177578..554d94ad50 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -23,8 +23,9 @@ HTML end def paragraph(text) - if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)/ + if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ convert_notes(text) + elsif text.include?('DO NOT READ THIS FILE ON GITHUB') elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/ linkback = %(<a href="#footnote-#{$1}-ref"><sup>#{$1}</sup></a>) %(<p class="footnote" id="footnote-#{$1}">#{linkback} #{$2}</p>) @@ -47,7 +48,7 @@ HTML case code_type when 'ruby', 'sql', 'plain' code_type - when 'erb' + when 'erb', 'html+erb' 'ruby; html-script: true' when 'html' 'xml' # HTML is understood, but there are .xml rules in the CSS diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index b11aaa15a8..be00087f63 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -1,3 +1,5 @@ +**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 20566c9155..328656f4a4 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 2.3 Release Notes =============================== diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 8122d6c235..9ad32e8168 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 3.0 Release Notes =============================== @@ -138,7 +140,7 @@ More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/r [Arel](http://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record. -More information: - [Why I wrote Arel](http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/.) +More information: - [Why I wrote Arel](https://web.archive.org/web/20120718093140/http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/) ### Mail Extraction @@ -298,7 +300,7 @@ Deprecations More Information: * [The Rails 3 Router: Rack it Up](http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up/) -* [Revamped Routes in Rails 3](http://rizwanreza.com/2009/12/20/revamped-routes-in-rails-3) +* [Revamped Routes in Rails 3](https://medium.com/fusion-of-thoughts/revamped-routes-in-rails-3-b6d00654e5b0) * [Generic Actions in Rails 3](http://yehudakatz.com/2009/12/20/generic-actions-in-rails-3/) @@ -545,7 +547,7 @@ These are the main changes in Active Support: * `String#to_time` and `String#to_datetime` handle fractional seconds. * Added support to new callbacks for around filter object that respond to `:before` and `:after` used in before and after callbacks. * The `ActiveSupport::OrderedHash#to_a` method returns an ordered set of arrays. Matches Ruby 1.9's `Hash#to_a`. -* `MissingSourceFile` exists as a constant but it is now just equals to `LoadError`. +* `MissingSourceFile` exists as a constant but it is now just equal to `LoadError`. * Added `Class#class_attribute`, to be able to declare a class-level attribute whose value is inheritable and overwritable by subclasses. * Finally removed `DeprecatedCallbacks` in `ActiveRecord::Associations`. * `Object#metaclass` is now `Kernel#singleton_class` to match Ruby. diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index b7ed285b96..e187e5f9ab 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 3.1 Release Notes =============================== diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index c5db0262e9..6ddf77d9c0 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -1,3 +1,5 @@ +**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 1aaf5ebc94..67f4a3c02c 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.0 Release Notes =============================== @@ -229,11 +231,11 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default` * New method `reversible` makes it possible to specify code to be run when migrating up or down. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method) + See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/active_record_migrations.md#using-reversible) * New method `revert` will revert a whole migration or the given block. If migrating down, the given migration / block is run normally. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations) + See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/active_record_migrations.md#reverting-previous-migrations) * Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support. diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 52a5acb75e..8d5557be6e 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.1 Release Notes =============================== @@ -136,7 +138,7 @@ end ### Action Mailer Previews -Action Mailer previews provide a way to visually see how emails look by visiting +Action Mailer previews provide a way to see how emails look by visiting a special URL that renders them. You implement a preview class whose methods return the mail object you'd like diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index cb461feff8..366d9d26b4 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -1,23 +1,23 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.2 Release Notes =============================== Highlights in Rails 4.2: -* Active Job, Action Mailer #deliver_later +* Active Job +* Asynchronous mails * Adequate Record * Web Console * Foreign key support -These release notes cover only the major changes. To learn about various bug -fixes and changes, please refer to the change logs or check out the [list of -commits](https://github.com/rails/rails/commits/master) in the main Rails -repository on GitHub. +These release notes cover only the major changes. To learn about other +features, bug fixes, and changes, please refer to the changelogs or check out +the [list of commits](https://github.com/rails/rails/commits/4-2-stable) in +the main Rails repository on GitHub. -------------------------------------------------------------------------------- -NOTE: This document is a work in progress, please help to improve this by sending -a [pull request](https://github.com/rails/rails/edit/master/guides/source/4_2_release_notes.md). - Upgrading to Rails 4.2 ---------------------- @@ -25,60 +25,115 @@ If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 4.1 in case you haven't and make sure your application still runs as expected before attempting to upgrade to Rails 4.2. A list of things to watch out for when upgrading is -available in the -[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2) -guide. +available in the guide [Upgrading Ruby on +Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2). Major Features -------------- -### Active Job, Action Mailer #deliver_later +### Active Job + +Active Job is a new framework in Rails 4.2. It is a common interface on top of +queuing systems like [Resque](https://github.com/resque/resque), [Delayed +Job](https://github.com/collectiveidea/delayed_job), +[Sidekiq](https://github.com/mperham/sidekiq), and more. + +Jobs written with the Active Job API run on any of the supported queues thanks +to their respective adapters. Active Job comes pre-configured with an inline +runner that executes jobs right away. + +Jobs often need to take Active Record objects as arguments. Active Job passes +object references as URIs (uniform resource identifiers) instead of marshaling +the object itself. The new [Global ID](https://github.com/rails/globalid) +library builds URIs and looks up the objects they reference. Passing Active +Record objects as job arguments just works by using Global ID internally. + +For example, if `trashable` is an Active Record object, then this job runs +just fine with no serialization involved: + +```ruby +class TrashableCleanupJob < ActiveJob::Base + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +See the [Active Job Basics](active_job_basics.html) guide for more +information. -Active Job is a new framework in Rails 4.2. It is an adapter layer on top of -queuing systems like [Resque](https://github.com/resque/resque), [Delayed Job](https://github.com/collectiveidea/delayed_job), [Sidekiq](https://github.com/mperham/sidekiq), and more. You can write your -jobs with the Active Job API, and it'll run on all these queues with no changes -(it comes pre-configured with an inline runner). +### Asynchronous Mails -Building on top of Active Job, Action Mailer now comes with a `#deliver_later` -method, which adds your email to be sent as a job to a queue, so it doesn't -bog down the controller or model. +Building on top of Active Job, Action Mailer now comes with a `deliver_later` +method that sends emails via the queue, so it doesn't block the controller or +model if the queue is asynchronous (the default inline queue blocks). -The new GlobalID library makes it easy to pass Active Record objects to jobs by -serializing them in a generic form. This means you no longer have to manually -pack and unpack your Active Records by passing ids. Just give the job the -straight Active Record object, and it'll serialize it using GlobalID, and -deserialize it at run time. +Sending emails right away is still possible with `deliver_now`. ### Adequate Record -Rails 4.2 comes with a performance improvement feature called Adequate Record -for Active Record. A lot of common queries are now up to twice as fast in Rails -4.2! +Adequate Record is a set of performance improvements in Active Record that makes +common `find` and `find_by` calls and some association queries up to 2x faster. -TODO: add some technical details +It works by caching common SQL queries as prepared statements and reusing them +on similar calls, skipping most of the query-generation work on subsequent +calls. For more details, please refer to [Aaron Patterson's blog +post](http://tenderlovemaking.com/2014/02/19/adequaterecord-pro-like-activerecord.html). -### Web Console +Active Record will automatically take advantage of this feature on +supported operations without any user involvement or code changes. Here are +some examples of supported operations: + +```ruby +Post.find(1) # First call generates and cache the prepared statement +Post.find(2) # Subsequent calls reuse the cached prepared statement + +Post.find_by_title('first post') +Post.find_by_title('second post') -New applications generated from Rails 4.2 now come with the Web Console gem by -default. +Post.find_by(title: 'first post') +Post.find_by(title: 'second post') -Web Console is a set of debugging tools for your Rails application. It will add -an interactive console on every error page, a `console` view helper and a VT100 -compatible terminal. +post.comments +post.comments(true) +``` -The interactive console on the error pages lets you execute code where the -exception originated. It's quite handy to introspect the state that led to the -error. +It's important to highlight that, as the examples above suggest, the prepared +statements do not cache the values passed in the method calls; rather, they +have placeholders for them. -The `console` view helper launches an interactive console within the context of -the view where it is invoked. +Caching is not used in the following scenarios: -Finally, you can launch a VT100 terminal that runs `rails console`. If you need -to create or modify existing test data, you can do that straight from the -browser. +- The model has a default scope +- The model uses single table inheritance +- `find` with a list of ids, e.g.: + + ```ruby + # not cached + Post.find(1, 2, 3) + Post.find([1,2]) + ``` + +- `find_by` with SQL fragments: + + ```ruby + Post.find_by('published_at < ?', 2.weeks.ago) + ``` + +### Web Console -### Foreign key support +New applications generated with Rails 4.2 now come with the [Web +Console](https://github.com/rails/web-console) gem by default. Web Console adds +an interactive Ruby console on every error page and provides a `console` view +and controller helpers. + +The interactive console on error pages lets you execute code in the context of +the place where the exception originated. The `console` helper, if called +anywhere in a view or controller, launches an interactive console with the final +context, once rendering has completed. + +### Foreign Key Support The migration DSL now supports adding and removing foreign keys. They are dumped to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql` @@ -113,11 +168,18 @@ individual components for new deprecations in this release. The following changes may require immediate action upon upgrade. -### `respond_with` / class-level `respond_to` +### `render` with a String Argument + +Previously, calling `render "foo/bar"` in a controller action was equivalent to +`render file: "foo/bar"`. In Rails 4.2, this has been changed to mean +`render template: "foo/bar"` instead. If you need to render a file, please +change your code to use the explicit form (`render file: "foo/bar"`) instead. -`respond_with` and the corresponding class-level `respond_to` have been moved to -the `responders` gem. To use the following, add `gem 'responders', '~> 2.0'` to -your Gemfile: +### `respond_with` / Class-Level `respond_to` + +`respond_with` and the corresponding class-level `respond_to` have been moved +to the [responders](https://github.com/plataformatec/responders) gem. Add +`gem 'responders', '~> 2.0'` to your Gemfile to use it: ```ruby # app/controllers/users_controller.rb @@ -148,46 +210,47 @@ class UsersController < ApplicationController end ``` -### Production logging +### Default Host for `rails server` -The default log level in the `production` environment is now `:debug`. This -makes it consistent with the other environments, and ensures plenty of -information is available to diagnose problems. +Due to a [change in Rack](https://github.com/rack/rack/commit/28b014484a8ac0bbb388e7eaeeef159598ec64fc), +`rails server` now listens on `localhost` instead of `0.0.0.0` by default. This +should have minimal impact on the standard development workflow as both +http://127.0.0.1:3000 and http://localhost:3000 will continue to work as before +on your own machine. -It can be returned to the previous level, `:info`, in the environment -configuration: +However, with this change you will no longer be able to access the Rails +server from a different machine, for example if your development environment +is in a virtual machine and you would like to access it from the host machine. +In such cases, please start the server with `rails server -b 0.0.0.0` to +restore the old behavior. -```ruby -# config/environments/production.rb - -# Decrease the log volume. -config.log_level = :info -``` +If you do this, be sure to configure your firewall properly such that only +trusted machines on your network can access your development server. ### HTML Sanitizer The HTML sanitizer has been replaced with a new, more robust, implementation -built upon Loofah and Nokogiri. The new sanitizer is more secure and its -sanitization is more powerful and flexible. +built upon [Loofah](https://github.com/flavorjones/loofah) and +[Nokogiri](https://github.com/sparklemotion/nokogiri). The new sanitizer is +more secure and its sanitization is more powerful and flexible. -With a new sanitization algorithm, the sanitized output will change for certain +Due to the new algorithm, the sanitized output may be different for certain pathological inputs. -If you have particular need for the exact output of the old sanitizer, you can -add `rails-deprecated_sanitizer` to your Gemfile, and it will automatically -replace the new implementation. Because it is opt-in, the legacy gem will not -give deprecation warnings. +If you have a particular need for the exact output of the old sanitizer, you +can add the [rails-deprecated_sanitizer](https://github.com/kaspth/rails-deprecated_sanitizer) +gem to the `Gemfile`, to have the old behavior. The gem does not issue +deprecation warnings because it is opt-in. `rails-deprecated_sanitizer` will be supported for Rails 4.2 only; it will not be maintained for Rails 5.0. -See [the blog post](http://blog.plataformatec.com.br/2014/07/the-new-html-sanitizer-in-rails-4-2/) -for more detail on the changes in the new sanitizer. +See [this blog post](http://blog.plataformatec.com.br/2014/07/the-new-html-sanitizer-in-rails-4-2/) +for more details on the changes in the new sanitizer. ### `assert_select` -`assert_select` is now based on Nokogiri, making it (TODO: betterer). - +`assert_select` is now based on [Nokogiri](https://github.com/sparklemotion/nokogiri). As a result, some previously-valid selectors are now unsupported. If your application is using any of these spellings, you will need to update them: @@ -195,8 +258,13 @@ application is using any of these spellings, you will need to update them: non-alphanumeric characters. ``` - a[href=/] => a[href="/"] - a[href$=/] => a[href$="/"] + # before + a[href=/] + a[href$=/] + + # now + a[href="/"] + a[href$="/"] ``` * DOMs built from HTML source containing invalid HTML with improperly @@ -242,11 +310,24 @@ Please refer to the [Changelog][railties] for detailed changes. ### Removals +* The `--skip-action-view` option has been removed from the + app generator. ([Pull Request](https://github.com/rails/rails/pull/17042)) + * The `rails application` command has been removed without replacement. ([Pull Request](https://github.com/rails/rails/pull/11616)) ### Deprecations +* Deprecated missing `config.log_level` for production environments. + ([Pull Request](https://github.com/rails/rails/pull/16622)) + +* Deprecated `rake test:all` in favor of `rake test` as it now run all tests + in the `test` folder. + ([Pull Request](https://github.com/rails/rails/pull/17348)) + +* Deprecated `rake test:all:db` in favor of `rake test:db`. + ([Pull Request](https://github.com/rails/rails/pull/17348)) + * Deprecated `Rails::Rack::LogTailer` without replacement. ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce)) @@ -258,9 +339,6 @@ Please refer to the [Changelog][railties] for detailed changes. * Added a `required` option to the model generator for associations. ([Pull Request](https://github.com/rails/rails/pull/16062)) -* Introduced an `after_bundle` callback for use in Rails templates. - ([Pull Request](https://github.com/rails/rails/pull/16359)) - * Introduced the `x` namespace for defining custom configuration options: ```ruby @@ -293,27 +371,30 @@ Please refer to the [Changelog][railties] for detailed changes. namespace: my_app_development # config/production.rb - MyApp::Application.configure do + Rails.application.configure do config.middleware.use ExceptionNotifier, config_for(:exception_notification) end ``` ([Pull Request](https://github.com/rails/rails/pull/16129)) -* Introduced a `--skip-gems` option in the app generator to skip gems such as - `turbolinks` and `coffee-rails` that do not have their own specific flags. - ([Commit](https://github.com/rails/rails/commit/10565895805887d4faf004a6f71219da177f78b7)) +* Introduced a `--skip-turbolinks` option in the app generator to not generate + turbolinks integration. + ([Commit](https://github.com/rails/rails/commit/bf17c8a531bc8059d50ad731398002a3e7162a7d)) -* Introduced a `bin/setup` script to enable automated setup code when +* Introduced a `bin/setup` script as a convention for automated setup code when bootstrapping an application. ([Pull Request](https://github.com/rails/rails/pull/15189)) -* Changed default value for `config.assets.digest` to `true` in development. +* Changed the default value for `config.assets.digest` to `true` in development. ([Pull Request](https://github.com/rails/rails/pull/15155)) * Introduced an API to register new extensions for `rake notes`. ([Pull Request](https://github.com/rails/rails/pull/14379)) +* Introduced an `after_bundle` callback for use in Rails templates. + ([Pull Request](https://github.com/rails/rails/pull/16359)) + * Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`. ([Pull Request](https://github.com/rails/rails/pull/14101)) @@ -326,10 +407,11 @@ Please refer to the [Changelog][action-pack] for detailed changes. ### Removals -* `respond_with` and the class-level `respond_to` were removed from Rails and +* `respond_with` and the class-level `respond_to` have been removed from Rails and moved to the `responders` gem (version 2.0). Add `gem 'responders', '~> 2.0'` to your `Gemfile` to continue using these features. - ([Pull Request](https://github.com/rails/rails/pull/16526)) + ([Pull Request](https://github.com/rails/rails/pull/16526), + [More Details](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#responders)) * Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` in favor of `AbstractController::Helpers::MissingHelperError`. @@ -337,12 +419,15 @@ Please refer to the [Changelog][action-pack] for detailed changes. ### Deprecations +* Deprecated the `only_path` option on `*_path` helpers. + ([Commit](https://github.com/rails/rails/commit/aa1fadd48fb40dd9396a383696134a259aa59db9)) + * Deprecated `assert_tag`, `assert_no_tag`, `find_tag` and `find_all_tag` in favor of `assert_select`. ([Commit](https://github.com/rails/rails-dom-testing/commit/b12850bc5ff23ba4b599bf2770874dd4f11bf750)) * Deprecated support for setting the `:to` option of a router to a symbol or a - string that does not contain a `#` character: + string that does not contain a "#" character: ```ruby get '/posts', to: MyRackApp => (No change necessary) @@ -353,22 +438,22 @@ Please refer to the [Changelog][action-pack] for detailed changes. ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9)) -### Notable changes +* Deprecated support for string keys in URL helpers: -* Rails will now automatically include the template's digest in ETags. - ([Pull Request](https://github.com/rails/rails/pull/16527)) + ```ruby + # bad + root_path('controller' => 'posts', 'action' => 'index') -* `render nothing: true` or rendering a `nil` body no longer add a single - space padding to the response body. - ([Pull Request](https://github.com/rails/rails/pull/14883)) + # good + root_path(controller: 'posts', action: 'index') + ``` -* Introduced the `always_permitted_parameters` option to configure which - parameters are permitted globally. The default value of this configuration - is `['controller', 'action']`. - ([Pull Request](https://github.com/rails/rails/pull/15933)) + ([Pull Request](https://github.com/rails/rails/pull/17743)) + +### Notable changes -* The `*_filter` family methods have been removed from the documentation. Their - usage is discouraged in favor of the `*_action` family methods: +* The `*_filter` family of methods have been removed from the documentation. Their + usage is discouraged in favor of the `*_action` family of methods: ``` after_filter => after_action @@ -393,16 +478,28 @@ Please refer to the [Changelog][action-pack] for detailed changes. (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de), [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4)) -* Added HTTP method `MKCALENDAR` from RFC-4791 +* `render nothing: true` or rendering a `nil` body no longer add a single + space padding to the response body. + ([Pull Request](https://github.com/rails/rails/pull/14883)) + +* Rails now automatically includes the template's digest in ETags. + ([Pull Request](https://github.com/rails/rails/pull/16527)) + +* Segments that are passed into URL helpers are now automatically escaped. + ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) + +* Introduced the `always_permitted_parameters` option to configure which + parameters are permitted globally. The default value of this configuration + is `['controller', 'action']`. + ([Pull Request](https://github.com/rails/rails/pull/15933)) + +* Added the HTTP method `MKCALENDAR` from [RFC 4791](https://tools.ietf.org/html/rfc4791). ([Pull Request](https://github.com/rails/rails/pull/15121)) * `*_fragment.action_controller` notifications now include the controller and action name in the payload. ([Pull Request](https://github.com/rails/rails/pull/14137)) -* Segments that are passed into URL helpers are now automatically escaped. - ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) - * Improved the Routing Error page with fuzzy matching for route search. ([Pull Request](https://github.com/rails/rails/pull/14619)) @@ -410,28 +507,26 @@ Please refer to the [Changelog][action-pack] for detailed changes. ([Pull Request](https://github.com/rails/rails/pull/14280)) * When the Rails server is set to serve static assets, gzip assets will now be - served if the client supports it and a pre-generated gzip file (.gz) is on disk. + served if the client supports it and a pre-generated gzip file (`.gz`) is on disk. By default the asset pipeline generates `.gz` files for all compressible assets. Serving gzip files minimizes data transfer and speeds up asset requests. Always [use a CDN](http://guides.rubyonrails.org/asset_pipeline.html#cdns) if you are serving assets from your Rails server in production. ([Pull Request](https://github.com/rails/rails/pull/16466)) -* The way `assert_select` works has changed; specifically a different library - is used to interpret css selectors, build the transient DOM that the - selectors are applied against, and to extract the data from that DOM. These - changes should only affect edge cases. Examples: - * Values in attribute selectors may need to be quoted if they contain - non-alphanumeric characters. - * DOMs built from HTML source containing invalid HTML with improperly - nested elements may differ. - * If the data selected contains entities, the value selected for comparison - used to be raw (e.g. `AT&T`), and now is evaluated - (e.g. `AT&T`). +* When calling the `process` helpers in an integration test the path needs to have + a leading slash. Previously you could omit it but that was a byproduct of the + implementation and not an intentional feature, e.g.: + ```ruby + test "list all posts" do + get "/posts" + assert_response :success + end + ``` Action View -------------- +----------- Please refer to the [Changelog][action-view] for detailed changes. @@ -448,16 +543,20 @@ Please refer to the [Changelog][action-view] for detailed changes. ### Notable changes -* Introduced a `#{partial_name}_iteration` special local variable for use with - partials that are rendered with a collection. It provides access to the - current state of the iteration via the `#index`, `#size`, `#first?` and - `#last?` methods. - ([Pull Request](https://github.com/rails/rails/pull/7698)) +* `render "foo/bar"` now expands to `render template: "foo/bar"` instead of + `render file: "foo/bar"`. + ([Pull Request](https://github.com/rails/rails/pull/16888)) * The form helpers no longer generate a `<div>` element with inline CSS around the hidden fields. ([Pull Request](https://github.com/rails/rails/pull/14738)) +* Introduced a `#{partial_name}_iteration` special local variable for use with + partials that are rendered with a collection. It provides access to the + current state of the iteration via the `index`, `size`, `first?` and + `last?` methods. + ([Pull Request](https://github.com/rails/rails/pull/7698)) + * Placeholder I18n follows the same convention as `label` I18n. ([Pull Request](https://github.com/rails/rails/pull/16438)) @@ -472,11 +571,15 @@ Please refer to the [Changelog][action-mailer] for detailed changes. * Deprecated `*_path` helpers in mailers. Always use `*_url` helpers instead. ([Pull Request](https://github.com/rails/rails/pull/15840)) -* Deprecated `deliver` / `deliver!` in favour of `deliver_now` / `deliver_now!`. +* Deprecated `deliver` / `deliver!` in favor of `deliver_now` / `deliver_now!`. ([Pull Request](https://github.com/rails/rails/pull/16582)) ### Notable changes +* `link_to` and `url_for` generate absolute URLs by default in templates, + it is no longer needed to pass `only_path: false`. + ([Commit](https://github.com/rails/rails/commit/9685080a7677abfa5d288a81c3e078368c6bb67c)) + * Introduced `deliver_later` which enqueues a job on the application's queue to deliver emails asynchronously. ([Pull Request](https://github.com/rails/rails/pull/16485)) @@ -505,7 +608,7 @@ Please refer to the [Changelog][active-record] for detailed changes. * Removed unused `:timestamp` type. Transparently alias it to `:datetime` in all cases. Fixes inconsistencies when column types are sent outside of - `ActiveRecord`, such as for XML serialization. + Active Record, such as for XML serialization. ([Pull Request](https://github.com/rails/rails/pull/15184)) ### Deprecations @@ -513,112 +616,123 @@ Please refer to the [Changelog][active-record] for detailed changes. * Deprecated swallowing of errors inside `after_commit` and `after_rollback`. ([Pull Request](https://github.com/rails/rails/pull/16537)) -* Deprecated calling `DatabaseTasks.load_schema` without a connection. Use - `DatabaseTasks.load_schema_current` instead. - ([Commit](https://github.com/rails/rails/commit/f15cef67f75e4b52fd45655d7c6ab6b35623c608)) - -* Deprecated `Reflection#source_macro` without replacement as it is no longer - needed in Active Record. - ([Pull Request](https://github.com/rails/rails/pull/16373)) - * Deprecated broken support for automatic detection of counter caches on `has_many :through` associations. You should instead manually specify the counter cache on the `has_many` and `belongs_to` associations for the through records. ([Pull Request](https://github.com/rails/rails/pull/15754)) -* Deprecated `serialized_attributes` without replacement. - ([Pull Request](https://github.com/rails/rails/pull/15704)) - -* Deprecated returning `nil` from `column_for_attribute` when no column - exists. It will return a null object in Rails 5.0 - ([Pull Request](https://github.com/rails/rails/pull/15878)) - -* Deprecated using `.joins`, `.preload` and `.eager_load` with associations - that depends on the instance state (i.e. those defined with a scope that - takes an argument) without replacement. - ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) - * Deprecated passing Active Record objects to `.find` or `.exists?`. Call - `#id` on the objects first. + `id` on the objects first. (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270), [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7)) * Deprecated half-baked support for PostgreSQL range values with excluding beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion - is not fully possible because the Ruby range does not support excluded - beginnings. + is not fully possible because Ruby ranges do not support excluded beginnings. The current solution of incrementing the beginning is not correct and is now deprecated. For subtypes where we don't know how to increment - (e.g. `#succ` is not defined) it will raise an `ArgumentError` for ranges + (e.g. `succ` is not defined) it will raise an `ArgumentError` for ranges with excluding beginnings. - ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3)) -### Notable changes +* Deprecated calling `DatabaseTasks.load_schema` without a connection. Use + `DatabaseTasks.load_schema_current` instead. + ([Commit](https://github.com/rails/rails/commit/f15cef67f75e4b52fd45655d7c6ab6b35623c608)) -* The PostgreSQL adapter now supports the `JSONB` datatype in PostgreSQL 9.4+. - ([Pull Request](https://github.com/rails/rails/pull/16220)) +* Deprecated `sanitize_sql_hash_for_conditions` without replacement. Using a + `Relation` for performing queries and updates is the preferred API. + ([Commit](https://github.com/rails/rails/commit/d5902c9e)) -* The `#references` method in migrations now supports a `type` option for - specifying the type of the foreign key (e.g. `:uuid`). - ([Pull Request](https://github.com/rails/rails/pull/16231)) +* Deprecated `add_timestamps` and `t.timestamps` without passing the `:null` + option. The default of `null: true` will change in Rails 5 to `null: false`. + ([Pull Request](https://github.com/rails/rails/pull/16481)) -* Added a `:required` option to singular associations, which defines a - presence validation on the association. - ([Pull Request](https://github.com/rails/rails/pull/16056)) +* Deprecated `Reflection#source_macro` without replacement as it is no longer + needed in Active Record. + ([Pull Request](https://github.com/rails/rails/pull/16373)) -* Introduced `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the - record is invalid. - ([Pull Request](https://github.com/rails/rails/pull/8639)) +* Deprecated `serialized_attributes` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15704)) -* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, - meaning that it no longer retains the extra attributes from custom - `select`s. - ([Pull Request](https://github.com/rails/rails/pull/15866)) +* Deprecated returning `nil` from `column_for_attribute` when no column + exists. It will return a null object in Rails 5.0. + ([Pull Request](https://github.com/rails/rails/pull/15878)) -* Introduced the `bin/rake db:purge` task to empty the database for the - current environment. - ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) +* Deprecated using `.joins`, `.preload` and `.eager_load` with associations + that depend on the instance state (i.e. those defined with a scope that + takes an argument) without replacement. + ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) + +### Notable changes + +* `SchemaDumper` uses `force: :cascade` on `create_table`. This makes it + possible to reload a schema when foreign keys are in place. + +* Added a `:required` option to singular associations, which defines a + presence validation on the association. + ([Pull Request](https://github.com/rails/rails/pull/16056)) * `ActiveRecord::Dirty` now detects in-place changes to mutable values. - Serialized attributes on ActiveRecord models will no longer save when + Serialized attributes on Active Record models are no longer saved when unchanged. This also works with other types such as string columns and json columns on PostgreSQL. (Pull Requests [1](https://github.com/rails/rails/pull/15674), [2](https://github.com/rails/rails/pull/15786), [3](https://github.com/rails/rails/pull/15788)) -* Added support for `#pretty_print` in `ActiveRecord::Base` objects. - ([Pull Request](https://github.com/rails/rails/pull/15172)) +* Introduced the `db:purge` Rake task to empty the database for the + current environment. + ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) -* PostgreSQL and SQLite adapters no longer add a default limit of 255 +* Introduced `ActiveRecord::Base#validate!` that raises + `ActiveRecord::RecordInvalid` if the record is invalid. + ([Pull Request](https://github.com/rails/rails/pull/8639)) + +* Introduced `validate` as an alias for `valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + +* `touch` now accepts multiple attributes to be touched at once. + ([Pull Request](https://github.com/rails/rails/pull/14423)) + +* The PostgreSQL adapter now supports the `jsonb` datatype in PostgreSQL 9.4+. + ([Pull Request](https://github.com/rails/rails/pull/16220)) + +* The PostgreSQL and SQLite adapters no longer add a default limit of 255 characters on string columns. ([Pull Request](https://github.com/rails/rails/pull/14579)) +* Added support for the `citext` column type in the PostgreSQL adapter. + ([Pull Request](https://github.com/rails/rails/pull/12523)) + +* Added support for user-created range types in the PostgreSQL adapter. + ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) + * `sqlite3:///some/path` now resolves to the absolute system path `/some/path`. For relative paths, use `sqlite3:some/path` instead. (Previously, `sqlite3:///some/path` resolved to the relative path - `some/path`. This behaviour was deprecated on Rails 4.1). + `some/path`. This behavior was deprecated on Rails 4.1). ([Pull Request](https://github.com/rails/rails/pull/14569)) -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) - -* `#touch` now accepts multiple attributes to be touched at once. - ([Pull Request](https://github.com/rails/rails/pull/14423)) - * Added support for fractional seconds for MySQL 5.6 and above. (Pull Request [1](https://github.com/rails/rails/pull/8240), [2](https://github.com/rails/rails/pull/14359)) -* Added support for the `citext` column type in PostgreSQL adapter. - ([Pull Request](https://github.com/rails/rails/pull/12523)) +* Added `ActiveRecord::Base#pretty_print` to pretty print models. + ([Pull Request](https://github.com/rails/rails/pull/15172)) -* Added support for user-created range types in PostgreSQL adapter. - ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) +* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, + meaning that it no longer retains the extra attributes from custom + `SELECT`s. + ([Pull Request](https://github.com/rails/rails/pull/15866)) +* `ActiveRecord::Base#reflections` now returns a hash with string keys instead + of symbol keys. ([Pull Request](https://github.com/rails/rails/pull/17718)) + +* The `references` method in migrations now supports a `type` option for + specifying the type of the foreign key (e.g. `:uuid`). + ([Pull Request](https://github.com/rails/rails/pull/16231)) Active Model ------------ @@ -636,17 +750,20 @@ Please refer to the [Changelog][active-model] for detailed changes. ([Pull Request](https://github.com/rails/rails/pull/16180)) * Deprecated `ActiveModel::Dirty#reset_changes` in favor of - `#clear_changes_information`. + `clear_changes_information`. ([Pull Request](https://github.com/rails/rails/pull/16180)) ### Notable changes +* Introduced `validate` as an alias for `valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + * Introduced the `restore_attributes` method in `ActiveModel::Dirty` to restore the changed (dirty) attributes to their previous values. (Pull Request [1](https://github.com/rails/rails/pull/14861), [2](https://github.com/rails/rails/pull/16180)) -* `has_secure_password` no longer disallow blank passwords (i.e. passwords +* `has_secure_password` no longer disallows blank passwords (i.e. passwords that contains only spaces) by default. ([Pull Request](https://github.com/rails/rails/pull/16412)) @@ -654,10 +771,6 @@ Please refer to the [Changelog][active-model] for detailed changes. characters if validations are enabled. ([Pull Request](https://github.com/rails/rails/pull/15708)) -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) - - Active Support -------------- @@ -688,10 +801,14 @@ Please refer to the [Changelog][active-support] for detailed changes. ### Notable changes -* Introduced new configuration option `active_support.test_order` for +* Introduced a new configuration option `active_support.test_order` for specifying the order test cases are executed. This option currently defaults to `:sorted` but will be changed to `:random` in Rails 5.0. - ([Commit](TODO: fill me in)) + ([Commit](https://github.com/rails/rails/commit/53e877f7d9291b2bf0b8c425f9e32ef35829f35b)) + +* `Object#try` and `Object#try!` can now be used without an explicit receiver in the block. + ([Commit](https://github.com/rails/rails/commit/5e51bdda59c9ba8e5faf86294e3e431bd45f1830), + [Pull Request](https://github.com/rails/rails/pull/17361)) * The `travel_to` test helper now truncates the `usec` component to 0. ([Commit](https://github.com/rails/rails/commit/9f6e82ee4783e491c20f5244a613fdeb4024beb5)) @@ -700,7 +817,7 @@ Please refer to the [Changelog][active-support] for detailed changes. (Commit [1](https://github.com/rails/rails/commit/702ad710b57bef45b081ebf42e6fa70820fdd810), [2](https://github.com/rails/rails/commit/64d91122222c11ad3918cc8e2e3ebc4b0a03448a)) -* `Object#with_options` can now be used without an explicit receiver. +* `Object#with_options` can now be used without an explicit receiver in the block. ([Pull Request](https://github.com/rails/rails/pull/16339)) * Introduced `String#truncate_words` to truncate a string by a number of words. @@ -719,6 +836,7 @@ Please refer to the [Changelog][active-support] for detailed changes. `module Foo; extend ActiveSupport::Concern; end` boilerplate. ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) +* New [guide](constant_autoloading_and_reloading.html) about constant autoloading and reloading. Credits ------- diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index f84f1cb376..67f5f1cdd5 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -10,10 +10,15 @@ </p> <% else %> <p> - These are the new guides for Rails 4.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 5.0 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> - The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.4/">Rails 4.1.4</a>, <a href="http://guides.rubyonrails.org/v4.0.8/">Rails 4.0.8</a>, <a href="http://guides.rubyonrails.org/v3.2.19/">Rails 3.2.19</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. +The guides for earlier releases: +<a href="http://guides.rubyonrails.org/v4.2.0/">Rails 4.2.0</a>, +<a href="http://guides.rubyonrails.org/v4.1.8/">Rails 4.1.8</a>, +<a href="http://guides.rubyonrails.org/v4.0.12/">Rails 4.0.12</a>, +<a href="http://guides.rubyonrails.org/v3.2.21/">Rails 3.2.21</a> and +<a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index ca1e79d3ce..f68179841e 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action Controller Overview ========================== @@ -112,8 +114,8 @@ NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&id 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. -NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced -with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) +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: @@ -735,7 +737,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 @@ -992,6 +994,11 @@ you would like in a response object. The `ActionController::Live` module allows you to create a persistent connection with a browser. Using this module, you will be able to send arbitrary data to the browser at specific points in time. +NOTE: The default Rails server (WEBrick) is a buffering web server and does not +support streaming. In order to use this feature, you'll need to use a non buffering +server like [Puma](http://puma.io), [Rainbows](http://rainbows.bogomips.org) +or [Passenger](https://www.phusionpassenger.com). + #### Incorporating Live Streaming Including `ActionController::Live` inside of your controller class will provide @@ -1164,65 +1171,10 @@ class ClientsController < ApplicationController end ``` -WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page). +WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information. - -### Custom errors page - -You can customize the layout of your error handling using controllers and views. -First define your app own routes to display the errors page. - -* `config/application.rb` - - ```ruby - config.exceptions_app = self.routes - ``` - -* `config/routes.rb` - - ```ruby - match '/404', via: :all, to: 'errors#not_found' - match '/422', via: :all, to: 'errors#unprocessable_entity' - match '/500', via: :all, to: 'errors#server_error' - ``` - -Create the controller and views. - -* `app/controllers/errors_controller.rb` - - ```ruby - class ErrorsController < ActionController::Base - layout 'error' - - def not_found - render status: :not_found - end - - def unprocessable_entity - render status: :unprocessable_entity - end - - def server_error - render status: :server_error - end - end - ``` - -* `app/views` - - ``` - errors/ - not_found.html.erb - unprocessable_entity.html.erb - server_error.html.erb - layouts/ - error.html.erb - ``` - -Do not forget to set the correct status code on the controller as shown before. You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues. - Force HTTPS protocol -------------------- diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index f981d0da47..73b240ff2c 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action Mailer Basics ==================== @@ -35,10 +37,26 @@ views. ```bash $ bin/rails generate mailer UserMailer create app/mailers/user_mailer.rb +create app/mailers/application_mailer.rb invoke erb create app/views/user_mailer +create app/views/layouts/mailer.text.erb +create app/views/layouts/mailer.html.erb invoke test_unit create test/mailers/user_mailer_test.rb +create test/mailers/previews/user_mailer_preview.rb +``` + +```ruby +# app/mailers/application_mailer.rb +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout 'mailer' +end + +# app/mailers/user_mailer.rb +class UserMailer < ApplicationMailer +end ``` As you can see, you can generate mailers just like you use other generators with @@ -63,8 +81,7 @@ delivered via email. `app/mailers/user_mailer.rb` contains an empty mailer: ```ruby -class UserMailer < ActionMailer::Base - default from: 'from@example.com' +class UserMailer < ApplicationMailer end ``` @@ -72,7 +89,7 @@ Let's add a method called `welcome_email`, that will send an email to the user's registered email address: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -298,8 +315,7 @@ Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in p ```html+erb <p>Hello there, this is our image</p> - <%= image_tag attachments['image.jpg'].url, alt: 'My Photo', - class: 'photos' %> + <%= image_tag attachments['image.jpg'].url, alt: 'My Photo', class: 'photos' %> ``` #### Sending Email To Multiple Recipients @@ -349,7 +365,7 @@ for the HTML version and `welcome_email.text.erb` for the plain text version. To change the default mailer view for your action you do something like: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -371,7 +387,7 @@ If you want more flexibility you can also pass a block and render specific templates or even render inline or text without using a template file: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -401,7 +417,7 @@ layout. In order to use a different file, call `layout` in your mailer: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer layout 'awesome' # use awesome.(html|text).erb as the layout end ``` @@ -413,7 +429,7 @@ You can also pass in a `layout: 'layout_name'` option to the render call inside the format block to specify different layouts for different formats: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user) mail(to: user.email) do |format| format.html { render layout: 'my_layout' } @@ -426,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 @@ -456,16 +505,7 @@ By using the full URL, your links will now work in your emails. #### generating URLs with `url_for` -You need to pass the `only_path: false` option when using `url_for`. This will -ensure that absolute URLs are generated because the `url_for` view helper will, -by default, generate relative URLs when a `:host` option isn't explicitly -provided. - -```erb -<%= url_for(controller: 'welcome', - action: 'greeting', - only_path: false) %> -``` +`url_for` generate full URL by default in templates. If you did not configure the `:host` option globally make sure to pass it to `url_for`. @@ -477,9 +517,6 @@ If you did not configure the `:host` option globally make sure to pass it to action: 'greeting') %> ``` -NOTE: When you explicitly pass the `:host` Rails will always generate absolute -URLs, so there is no need to pass `only_path: false`. - #### generating URLs with named routes Email clients have no web context and so paths have no base URL to form complete @@ -511,7 +548,7 @@ while delivering emails, you can do this using `delivery_method_options` in the mailer action. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user, company) @user = user @url = user_url(@user) @@ -533,7 +570,7 @@ option. In such cases don't forget to add the `:content_type` option. Rails will default to `text/plain` otherwise. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user, email_body) mail(to: user.email, body: email_body, @@ -563,7 +600,7 @@ mailer, and pass the email object to the mailer `receive` instance method. Here's an example: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def receive(email) page = Page.find_by(address: email.to.first) page.emails.create( @@ -599,7 +636,7 @@ Action Mailer allows for you to specify a `before_action`, `after_action` and using instance variables set in your mailer action. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer after_action :set_delivery_options, :prevent_delivery_to_guests, :set_business_headers @@ -729,7 +766,9 @@ Mailer framework. You can do this in an initializer file `config/initializers/sandbox_email_interceptor.rb` ```ruby -ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging? +if Rails.env.staging? + ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) +end ``` NOTE: The example above uses a custom environment called "staging" for a diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 683e633668..8f6676dc65 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action View Overview ==================== @@ -7,7 +9,6 @@ After reading this guide, you will know: * How best to use templates, partials, and layouts. * What helpers are provided by Action View and how to make your own. * How to use localized views. -* How to use Action View outside of Rails. -------------------------------------------------------------------------------- @@ -189,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: @@ -348,83 +365,6 @@ WIP: Not all the helpers are listed here. For a full list see the [API documenta 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 - -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. - -#### content_tag_for - -Renders a container tag that relates to your Active Record Object. - -For example, given `@article` is the object of `Article` class, you can do: - -```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> -``` - -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: - -```html+erb -<%= content_tag_for(:tr, @articles) do |article| %> - <td><%= article.title %></td> -<% end %> -``` - -Will generate this HTML output: - -```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> -``` - -#### div_for - -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 %> -``` - -Will generate this HTML output: - -```html -<div id="article_1234" class="article frontpage"> - <td>Hello World!</td> -</div> -``` - ### AssetTagHelper This module provides methods for generating HTML that links views to assets such as images, JavaScript files, stylesheets, and feeds. @@ -1600,7 +1540,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 7b3081993d..953c29719d 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Job Basics ================= @@ -23,7 +25,7 @@ clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really. -The Purpose of the Active Job +The Purpose of Active Job ----------------------------- The main point is to ensure that all Rails apps will have a job infrastructure in place, even if it's in the form of an "immediate runner". We can then have @@ -41,10 +43,12 @@ This section will provide a step-by-step guide to creating a job and enqueuing i ### Create the Job Active Job provides a Rails generator to create jobs. The following will create a -job in `app/jobs`: +job in `app/jobs` (with an attached test case under `test/jobs`): ```bash $ bin/rails generate job guests_cleanup +invoke test_unit +create test/jobs/guests_cleanup_job_test.rb create app/jobs/guests_cleanup_job.rb ``` @@ -52,12 +56,8 @@ You can also create a job that will run on a specific queue: ```bash $ bin/rails generate job guests_cleanup --queue urgent -create app/jobs/guests_cleanup_job.rb ``` -As you can see, you can generate jobs just like you use other generators with -Rails. - If you don't want to use a generator, you could create your own file inside of `app/jobs`, just make sure that it inherits from `ActiveJob::Base`. @@ -78,15 +78,19 @@ end Enqueue a job like so: ```ruby -MyJob.perform_later record # 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 ``` ```ruby -MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record) # Enqueue a job to be performed tomorrow at noon. +# Enqueue a job to be performed tomorrow at noon. +MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record) ``` ```ruby -MyJob.set(wait: 1.week).perform_later(record) # Enqueue a job to be performed 1 week from now. +# Enqueue a job to be performed 1 week from now. +MyJob.set(wait: 1.week).perform_later(record) ``` That's it! @@ -99,46 +103,24 @@ If no adapter is set, the job is immediately executed. ### Backends -Active Job has adapters for the following queueing backends: - -* [Backburner](https://github.com/nesquena/backburner) -* [Delayed Job](https://github.com/collectiveidea/delayed_job) -* [Qu](https://github.com/bkeepers/qu) -* [Que](https://github.com/chanks/que) -* [QueueClassic 2.x](https://github.com/ryandotsmith/queue_classic/tree/v2.2.3) -* [Resque 1.x](https://github.com/resque/resque/tree/1-x-stable) -* [Sidekiq](https://github.com/mperham/sidekiq) -* [Sneakers](https://github.com/jondot/sneakers) -* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch) - -#### Backends Features - -| | Async | Queues | Delayed | Priorities | Timeout | Retries | -|-----------------------|-------|--------|-----------|------------|---------|---------| -| **Backburner** | Yes | Yes | Yes | Yes | Job | Global | -| **Delayed Job** | Yes | Yes | Yes | Job | Global | Global | -| **Que** | Yes | Yes | Yes | Job | No | Job | -| **Queue Classic** | Yes | Yes | No* | No | No | No | -| **Resque** | Yes | Yes | Yes (Gem) | Queue | Global | Yes | -| **Sidekiq** | Yes | Yes | Yes | Queue | No | Job | -| **Sneakers** | Yes | Yes | No | Queue | Queue | No | -| **Sucker Punch** | Yes | Yes | No | No | No | No | -| **Active Job Inline** | No | Yes | N/A | N/A | N/A | N/A | -| **Active Job** | Yes | Yes | Yes | No | No | No | - -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. - -### Change Backends - -You can easily change your adapter: +Active Job has built-in adapters for multiple queueing backends (Sidekiq, +Resque, Delayed Job and others). To get an up-to-date list of the adapters +see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + +### Setting the Backend + +You can easily set your queueing backend: ```ruby -# be sure to have the adapter gem in your Gemfile and follow the adapter specific -# installation and deployment instructions -YourApp::Application.config.active_job.queue_adapter = :sidekiq +# 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. + config.active_job.queue_adapter = :sidekiq + end +end ``` @@ -173,19 +155,42 @@ class GuestsCleanupJob < ActiveJob::Base end # Now your job will run on queue production_low_priority on your -# production environment and on beta_low_priority on your beta -# 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 +`config.active_job.queue_name_delimiter` in `application.rb`: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + config.active_job.queue_name_prefix = Rails.env + config.active_job.queue_name_delimiter = '.' + end +end + +# app/jobs/guests_cleanup.rb +class GuestsCleanupJob < ActiveJob::Base + queue_as :low_priority + #.... +end + +# Now your job will run on queue production.low_priority on your +# 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 -option to #set: +If you want more control on what queue a job will be run you can pass a `:queue` +option to `#set`: ```ruby MyJob.set(queue: :another_queue).perform_later(record) ``` -To control the queue from the job level you can pass a block to queue_as. The -block will be executed in the job context (so you can access self.arguments) +To control the queue from the job level you can pass a block to `#queue_as`. The +block will be executed in the job context (so you can access `self.arguments`) and you must return the queue name: ```ruby @@ -200,14 +205,13 @@ class ProcessVideoJob < ActiveJob::Base end def perform(video) - # do process video + # Do process video end end ProcessVideoJob.perform_later(Video.last) ``` - NOTE: Make sure your queueing backend "listens" on your queue name. For some backends you need to specify the queues to listen to. @@ -215,8 +219,8 @@ backends you need to specify the queues to listen to. Callbacks --------- -Active Job provides hooks during the lifecycle of a job. Callbacks allow you to -trigger logic during the lifecycle of a job. +Active Job provides hooks during the life cycle of a job. Callbacks allow you to +trigger logic during the life cycle of a job. ### Available callbacks @@ -234,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 @@ -250,7 +254,7 @@ end ``` -ActionMailer +Action Mailer ------------ One of the most common jobs in a modern web application is sending emails outside @@ -268,12 +272,13 @@ UserMailer.welcome(@user).deliver_later GlobalID -------- + Active Job supports GlobalID for parameters. This makes it possible to pass live Active Record objects to your job instead of class/id pairs, which you then have to manually deserialize. Before, jobs would look like this: ```ruby -class TrashableCleanupJob +class TrashableCleanupJob < ActiveJob::Base def perform(trashable_class, trashable_id, depth) trashable = trashable_class.constantize.find(trashable_id) trashable.cleanup(depth) @@ -284,15 +289,15 @@ end Now you can simply do: ```ruby -class TrashableCleanupJob +class TrashableCleanupJob < ActiveJob::Base def perform(trashable, depth) trashable.cleanup(depth) end end ``` -This works with any class that mixes in `ActiveModel::GlobalIdentification`, which -by default has been mixed into Active Model classes. +This works with any class that mixes in `GlobalID::Identification`, which +by default has been mixed into Active Record classes. Exceptions @@ -302,12 +307,11 @@ Active Job provides a way to catch exceptions raised during the execution of the job: ```ruby - 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 diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 3eaeeff389..4b2bfaee2f 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -1,20 +1,34 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Model Basics =================== -This guide should provide you with all you need to get started using model classes. Active Model allows for Action Pack helpers to interact with non-Active Record models. Active Model also helps building custom ORMs for use outside of the Rails framework. +This guide should provide you with all you need to get started using model +classes. Active Model allows for Action Pack helpers to interact with +plain Ruby objects. Active Model also helps build custom ORMs for use +outside of the Rails framework. + +After reading this guide, you will be able to add to plain Ruby objects: -After reading this guide, you will know: +* The ability to behave like an Active Record model. +* Callbacks and validations like Active Record. +* Serializers. +* Integration with the Rails internationalization (i18n) framework. -------------------------------------------------------------------------------- Introduction ------------ -Active Model is a library containing various modules used in developing frameworks that need to interact with the Rails Action Pack library. Active Model provides a known set of interfaces for usage in classes. Some of modules are explained below. +Active Model is a library containing various modules used in developing +classes that need some features present on Active Record. +Some of these modules are explained below. -### AttributeMethods +### Attribute Methods -The AttributeMethods module can add custom prefixes and suffixes on methods of a class. It is used by defining the prefixes and suffixes and which methods on the object will use them. +The `ActiveModel::AttributeMethods` module can add custom prefixes and suffixes +on methods of a class. It is used by defining the prefixes and suffixes and +which methods on the object will use them. ```ruby class Person @@ -38,14 +52,17 @@ end person = Person.new person.age = 110 -person.age_highest? # true -person.reset_age # 0 -person.age_highest? # false +person.age_highest? # => true +person.reset_age # => 0 +person.age_highest? # => false ``` ### Callbacks -Callbacks gives Active Record style callbacks. This provides an ability to define callbacks which run at appropriate times. After defining callbacks, you can wrap them with before, after and around custom methods. +`ActiveModel::Callbacks` gives Active Record style callbacks. This provides an +ability to define callbacks which run at appropriate times. +After defining callbacks, you can wrap them with before, after and around +custom methods. ```ruby class Person @@ -69,7 +86,9 @@ end ### Conversion -If a class defines `persisted?` and `id` methods, then you can include the `Conversion` module in that class and call the Rails conversion methods on objects of that class. +If a class defines `persisted?` and `id` methods, then you can include the +`ActiveModel::Conversion` module in that class and call the Rails conversion +methods on objects of that class. ```ruby class Person @@ -92,11 +111,13 @@ person.to_param # => nil ### Dirty -An object becomes dirty when it has gone through one or more changes to its attributes and has not been saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Let's consider a Person class with attributes `first_name` and `last_name`: +An object becomes dirty when it has gone through one or more changes to its +attributes and has not been saved. `ActiveModel::Dirty` gives the ability to +check whether an object has been changed or not. It also has attribute based +accessor methods. Let's consider a Person class with attributes `first_name` +and `last_name`: ```ruby -require 'active_model' - class Person include ActiveModel::Dirty define_attribute_methods :first_name, :last_name @@ -162,10 +183,11 @@ Track what was the previous value of the attribute. ```ruby # attr_name_was accessor -person.first_name_was # => "First Name" +person.first_name_was # => nil ``` -Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil. +Track both previous and current value of the changed attribute. Returns an array +if changed, else returns nil. ```ruby # attr_name_change @@ -175,7 +197,8 @@ person.last_name_change # => nil ### Validations -Validations module adds the ability to class objects to validate them in Active Record style. +`ActiveModel::Validations` module adds the ability to validate class objects +like in Active Record. ```ruby class Person @@ -188,7 +211,8 @@ class Person validates! :token, presence: true end -person = Person.new(token: "2b1f325") +person = Person.new +person.token = "2b1f325" person.valid? # => false person.name = 'vishnu' person.email = 'me' @@ -199,9 +223,9 @@ person.token = nil person.valid? # => raises ActiveModel::StrictValidationFailed ``` -### ActiveModel::Naming +### Naming -Naming adds a number of class methods which make the naming and routing +`ActiveModel::Naming` adds a number of class methods which make the naming and routing easier to manage. The module defines the `model_name` class method which will define a number of accessors using some `ActiveSupport::Inflector` methods. @@ -221,3 +245,312 @@ Person.model_name.i18n_key # => :person Person.model_name.route_key # => "people" Person.model_name.singular_route_key # => "person" ``` + +### Model + +`ActiveModel::Model` adds the ability to a class to work with Action Pack and +Action View right out of the box. + +```ruby +class EmailContact + include ActiveModel::Model + + attr_accessor :name, :email, :message + validates :name, :email, :message, presence: true + + def deliver + if valid? + # deliver email + end + end +end +``` + +When including `ActiveModel::Model` you get some features like: + +- model name introspection +- conversions +- translations +- validations + +It also gives you the ability to initialize an object with a hash of attributes, +much like any Active Record object. + +```ruby +email_contact = EmailContact.new(name: 'David', + email: 'david@example.com', + message: 'Hello World') +email_contact.name # => 'David' +email_contact.email # => 'david@example.com' +email_contact.valid? # => true +email_contact.persisted? # => false +``` + +Any class that includes `ActiveModel::Model` can be used with `form_for`, +`render` and any other Action View helper methods, just like Active Record +objects. + +### Serialization + +`ActiveModel::Serialization` provides a basic serialization for your object. +You need to declare an attributes hash which contains the attributes you want to +serialize. Attributes must be strings, not symbols. + +```ruby +class Person + include ActiveModel::Serialization + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +Now you can access a serialized hash of your object using the `serializable_hash`. + +```ruby +person = Person.new +person.serializable_hash # => {"name"=>nil} +person.name = "Bob" +person.serializable_hash # => {"name"=>"Bob"} +``` + +#### ActiveModel::Serializers + +Rails provides two serializers `ActiveModel::Serializers::JSON` and +`ActiveModel::Serializers::Xml`. Both of these modules automatically include +the `ActiveModel::Serialization`. + +##### ActiveModel::Serializers::JSON + +To use the `ActiveModel::Serializers::JSON` you only need to change from +`ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`. + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +With the `as_json` you have a hash representing the model. + +```ruby +person = Person.new +person.as_json # => {"name"=>nil} +person.name = "Bob" +person.as_json # => {"name"=>"Bob"} +``` + +From a JSON string you define the attributes of the model. +You need to have the `attributes=` method defined on your class: + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes=(hash) + hash.each do |key, value| + send("#{key}=", value) + end + end + + def attributes + {'name' => nil} + end +end +``` + +Now it is possible to create an instance of person and set the attributes using `from_json`. + +```ruby +json = { name: 'Bob' }.to_json +person = Person.new +person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob"> +person.name # => "Bob" +``` + +##### ActiveModel::Serializers::Xml + +To use the `ActiveModel::Serializers::Xml` you only need to change from +`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`. + +```ruby +class Person + include ActiveModel::Serializers::Xml + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +With the `to_xml` you have an XML representing the model. + +```ruby +person = Person.new +person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n" +person.name = "Bob" +person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n" +``` + +From an XML string you define the attributes of the model. +You need to have the `attributes=` method defined on your class: + +```ruby +class Person + include ActiveModel::Serializers::Xml + + attr_accessor :name + + def attributes=(hash) + hash.each do |key, value| + send("#{key}=", value) + end + end + + def attributes + {'name' => nil} + end +end +``` + +Now it is possible to create an instance of person and set the attributes using `from_xml`. + +```ruby +xml = { name: 'Bob' }.to_xml +person = Person.new +person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob"> +person.name # => "Bob" +``` + +### Translation + +`ActiveModel::Translation` provides integration between your object and the Rails +internationalization (i18n) framework. + +```ruby +class Person + extend ActiveModel::Translation +end +``` + +With the `human_attribute_name` you can transform attribute names into a more +human format. The human format is defined in your locale file. + +* config/locales/app.pt-BR.yml + + ```yml + pt-BR: + activemodel: + attributes: + person: + name: 'Nome' + ``` + +```ruby +Person.human_attribute_name('name') # => "Nome" +``` + +### Lint Tests + +`ActiveModel::Lint::Tests` allow you to test whether an object is compliant with +the Active Model API. + +* app/models/person.rb + + ```ruby + class Person + include ActiveModel::Model + + end + ``` + +* test/models/person_test.rb + + ```ruby + require 'test_helper' + + class PersonTest < ActiveSupport::TestCase + include ActiveModel::Lint::Tests + + def setup + @model = Person.new + end + end + ``` + +```bash +$ rake test + +Run options: --seed 14596 + +# Running: + +...... + +Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s. + +6 runs, 30 assertions, 0 failures, 0 errors, 0 skips +``` + +An object is not required to implement all APIs in order to work with +Action Pack. This module only intends to provide guidance in case you want all +features out of the box. + +### SecurePassword + +`ActiveModel::SecurePassword` provides a way to securely store any +password in an encrypted form. On including this module, a +`has_secure_password` class method is provided which defines +an accessor named `password` with certain validations on it. + +#### Requirements + +`ActiveModel::SecurePassword` depends on the [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'), +so include this gem in your Gemfile to use `ActiveModel::SecurePassword` correctly. +In order to make this work, the model must have an accessor named `password_digest`. +The `has_secure_password` will add the following validations on the `password` accessor: + +1. Password should be present. +2. Password should be equal to its confirmation. +3. This maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) + +#### Examples + +```ruby +class Person + include ActiveModel::SecurePassword + has_secure_password + attr_accessor :password_digest +end + +person = Person.new + +# When password is blank. +person.valid? # => false + +# When the confirmation doesn't match the password. +person.password = 'aditya' +person.password_confirmation = 'nomatch' +person.valid? # => false + +# When the length of password, exceeds 72. +person.password = person.password_confirmation = 'a' * 100 +person.valid? # => false + +# When all validations are passed. +person.password = person.password_confirmation = 'aditya' +person.valid? # => true +``` diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index eff93ce41d..bd8cdf62f2 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Basics ==================== @@ -31,12 +33,12 @@ Object Relational Mapping system. in his book _Patterns of Enterprise Application Architecture_. In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring -data access logic is part of the object will educate users of that +data access logic as part of the object will educate users of that 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 @@ -60,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 @@ -116,7 +118,7 @@ to Active Record instances: locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model. * `type` - Specifies that the model uses [Single Table - Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance). + Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance). * `(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 @@ -358,7 +360,7 @@ class CreatePublications < ActiveRecord::Migration t.string :publisher_type t.boolean :single_issue - t.timestamps + t.timestamps null: false end add_index :publications, :publication_type_id end diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 9c7e60cbb0..e65ab802c0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Callbacks ======================= diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 229c6ee458..b8db21a989 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Migrations ======================== @@ -39,7 +41,7 @@ class CreateProducts < ActiveRecord::Migration t.string :name t.text :description - t.timestamps + t.timestamps null: false end end end @@ -285,7 +287,7 @@ class CreateProducts < ActiveRecord::Migration t.string :name t.text :description - t.timestamps + t.timestamps null: false end end end @@ -452,6 +454,8 @@ number of digits after the decimal point. are using a dynamic value (such as a date), the default will only be calculated the first time (i.e. on the date the migration is applied). * `index` Adds an index for the column. +* `required` Adds `required: true` for `belongs_to` associations and +`null: false` to the column in the migration. Some adapters may support additional options; see the adapter specific API docs for further information. @@ -466,7 +470,7 @@ add_foreign_key :articles, :authors ``` This adds a new foreign key to the `author_id` column of the `articles` -table. The key references the `id` column of the `articles` table. If the +table. The key references the `id` column of the `authors` table. If the column names can not be derived from the table names, you can use the `:column` and `:primary_key` options. @@ -475,7 +479,8 @@ Rails will generate a name for every foreign key starting with 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: @@ -691,6 +696,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 ------------------ @@ -824,7 +833,7 @@ class CreateProducts < ActiveRecord::Migration create_table :products do |t| t.string :name t.text :description - t.timestamps + t.timestamps null: false end end @@ -939,10 +948,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 6c94218ef6..4d9c1776f4 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record and PostgreSQL ============================ @@ -83,9 +85,12 @@ Book.where("array_length(ratings, 1) >= 3") * [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) +NOTE: you need to enable the `hstore` extension to use hstore. + ```ruby # db/migrate/20131009135255_create_profiles.rb ActiveRecord::Schema.define do + enable_extension 'hstore' unless extension_enabled?('hstore') create_table :profiles do |t| t.hstore 'settings' end @@ -103,11 +108,6 @@ profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} profile.save! - -## you need to call _will_change! if you are editing the store in place -profile.settings["color"] = "green" -profile.settings_will_change! -profile.save! ``` ### JSON @@ -219,7 +219,7 @@ Currently there is no special support for enumerated types. They are mapped as normal text columns: ```ruby -# db/migrate/20131220144913_create_events.rb +# db/migrate/20131220144913_create_articles.rb execute <<-SQL CREATE TYPE article_status AS ENUM ('draft', 'published'); SQL @@ -281,7 +281,7 @@ end # Usage User.create settings: "01010011" user = User.first -user.settings # => "(Paris,Champs-Élysées)" +user.settings # => "01010011" user.settings = "0xAF" user.settings # => 10101111 user.save! diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index e1a465c64f..e5a962b739 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Query Interface ============================= @@ -9,6 +11,7 @@ After reading this guide, you will know: * How to specify the order, retrieved attributes, grouping, and other properties of the found records. * How to use eager loading to reduce the number of database queries needed for data retrieval. * How to use dynamic finders methods. +* How to use method chaining to use multiple ActiveRecord methods together. * How to check for the existence of particular records. * How to perform various calculations on Active Record models. * How to run EXPLAIN on relations. @@ -87,7 +90,7 @@ The primary operation of `Model.find(options)` can be summarized as: * Convert the supplied options to an equivalent SQL query. * Fire the SQL query and retrieve the corresponding results from the database. * Instantiate the equivalent Ruby object of the appropriate model for every resulting row. -* Run `after_find` callbacks, if any. +* Run `after_find` and then `after_initialize` callbacks, if any. ### Retrieving a Single Object @@ -254,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 @@ -308,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. +Two additional options, `:batch_size` and `:begin_at`, are available as well. **`:batch_size`** @@ -320,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 upto 10000 and to retrieve them in batches of 1000: + +```ruby +User.find_each(begin_at: 2000, end_at: 10000, batch_size: 5000) do |user| + NewsMailer.weekly(user).deliver_now +end +``` #### `find_in_batches` @@ -347,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 ---------- @@ -1125,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) @@ -1266,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 @@ -1327,14 +1349,67 @@ You can specify an exclamation point (`!`) on the end of the dynamic finders to If you want to find both by name and locked, you can chain these finders together by simply typing "`and`" between the fields. For example, `Client.find_by_first_name_and_locked("Ryan", true)`. +Understanding The Method Chaining +--------------------------------- + +The Active Record pattern implements [Method Chaining](http://en.wikipedia.org/wiki/Method_chaining), +which allow us to use multiple Active Record methods together in a simple and straightforward way. + +You can chain methods in a statement when the previous method called returns an +`ActiveRecord::Relation`, like `all`, `where`, and `joins`. Methods that return +a single object (see [Retrieving a Single Object Section](#retrieving-a-single-object)) +have to be at the end of the statement. + +There are some examples below. This guide won't cover all the possibilities, just a few as examples. +When an Active Record method is called, the query is not immediately generated and sent to the database, +this just happens when the data is actually needed. So each example below generates a single query. + +### Retrieving filtered data from multiple tables + +```ruby +Person + .select('people.id, people.name, comments.text') + .joins(:comments) + .where('comments.created_at > ?', 1.week.ago) +``` + +The result should be something like this: + +```sql +SELECT people.id, people.name, comments.text +FROM people +INNER JOIN comments + ON comments.person_id = people.id +WHERE comments.created_at = '2015-01-01' +``` + +### Retrieving specific data from multiple tables + +```ruby +Person + .select('people.id, people.name, companies.name') + .joins(:company) + .find_by('people.name' => 'John') # this should be the last +``` + +The above should generate: + +```sql +SELECT people.id, people.name, companies.name +FROM people +INNER JOIN companies + ON companies.person_id = people.id +WHERE people.name = 'John' +LIMIT 1 +``` + +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 -------------------------- -NOTE: Some dynamic finders have been deprecated in Rails 4.0 and will be -removed in Rails 4.1. The best practice is to use Active Record scopes -instead. You can find the deprecation gem at -https://github.com/rails/activerecord-deprecated_finders - It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods. ### `find_or_create_by` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 582bb240dd..d65c15bcf3 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Validations ========================= @@ -225,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 ------------------ @@ -361,6 +381,8 @@ class Product < ActiveRecord::Base end ``` +Alternatively, you can require that the specified attribute does _not_ match the regular expression by using the `:without` option. + The default error message is _"is invalid"_. ### `inclusion` @@ -425,7 +447,7 @@ class Essay < ActiveRecord::Base validates :content, length: { minimum: 300, maximum: 400, - tokenizer: lambda { |str| str.scan(/\w+/) }, + tokenizer: lambda { |str| str.split(/\s+/) }, too_short: "must have at least %{count} words", too_long: "must have at most %{count} words" } @@ -448,7 +470,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 @@ -485,6 +507,8 @@ constraints to acceptable values: * `:even` - Specifies the value must be an even number if set to true. The default error message for this option is _"must be even"_. +NOTE: By default, `numericality` doesn't allow `nil` values. You can use `allow_nil: true` option to permit it. + The default error message is _"is not a number"_. ### `presence` @@ -524,9 +548,16 @@ If you validate the presence of an object associated via a `has_one` or `marked_for_destruction?`. Since `false.blank?` is true, if you want to validate the presence of a boolean -field you should use `validates :field_name, inclusion: { in: [true, false] }`. +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] } +``` -The default error message is _"can't be blank"_. +By using one of these validations, you will ensure the value will NOT be `nil` +which would result in a `NULL` value in most cases. ### `absence` @@ -575,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 @@ -597,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/9.4/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 @@ -698,7 +728,7 @@ we don't want names and surnames to begin with lower case. ```ruby class Person < ActiveRecord::Base validates_each :name, :surname do |record, attr, value| - record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ + record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/ end end ``` @@ -1048,7 +1078,7 @@ Another way to do this is using `[]=` setter ```ruby class Person < ActiveRecord::Base def a_method_used_for_validation_purposes - errors[:name] = "cannot contain the characters !@#%*()_-+=" + errors.add(:name, "cannot contain the characters !@#%*()_-+=") end end @@ -1061,6 +1091,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 2e1a3ee5dc..0fbd6ed7e1 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Support Core Extensions ============================== @@ -465,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: @@ -473,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`: - -```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 @@ -1011,7 +997,7 @@ self.default_params = { }.freeze ``` -They can be also accessed and overridden at the instance level. +They can also be accessed and overridden at the instance level. ```ruby A.x = 1 @@ -1268,7 +1254,7 @@ The method `squish` strips leading and trailing whitespace, and substitutes runs There's also the destructive version `String#squish!`. -Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). +Note that it handles both ASCII and Unicode whitespace. NOTE: Defined in `active_support/core_ext/string/filters.rb`. @@ -1447,7 +1433,7 @@ Returns the substring of the string starting at position `position`: "hello".from(0) # => "hello" "hello".from(2) # => "llo" "hello".from(-2) # => "lo" -"hello".from(10) # => "" if < 1.9, nil in 1.9 +"hello".from(10) # => nil ``` NOTE: Defined in `active_support/core_ext/string/access.rb`. @@ -1833,16 +1819,14 @@ attribute names: ```ruby def full_messages - full_messages = [] - - each do |attribute, messages| - ... - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - ... - end + map { |attribute, message| full_message(attribute, message) } +end - full_messages +def full_message + ... + attr_name = attribute.to_s.tr('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, default: attr_name) + ... end ``` @@ -1952,24 +1936,6 @@ as well as adding or subtracting their results from a Time object. For example: (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: - -```ruby -# 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. - -NOTE: Defined in `active_support/core_ext/numeric/time.rb`. - ### Formatting Enables the formatting of numbers in a variety of ways. @@ -3831,50 +3797,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` ------------------------- @@ -3891,7 +3813,7 @@ def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" @@ -3903,7 +3825,7 @@ NOTE: Defined in `active_support/core_ext/name_error.rb`. Extensions to `LoadError` ------------------------- -Active Support adds `is_missing?` to `LoadError`, and also assigns that class to the constant `MissingSourceFile` for backwards compatibility. +Active Support adds `is_missing?` to `LoadError`. Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). @@ -3914,7 +3836,7 @@ def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 7033947468..9d9f40e956 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Support Instrumentation ============================== @@ -135,7 +137,9 @@ Action Controller | `:format` | html/js/json/xml etc | | `:method` | HTTP request verb | | `:path` | Request path | +| `:status` | HTTP status code | | `:view_runtime` | Amount spent in view in ms | +| `:db_runtime` | Amount spent executing database queries in ms | ```ruby { @@ -223,11 +227,11 @@ Active Record ### sql.active_record -| Key | Value | -| ------------ | --------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:object_id` | `self.object_id` | +| Key | Value | +| ---------------- | --------------------- | +| `:sql` | SQL statement | +| `:name` | Name of the operation | +| `:connection_id` | `self.object_id` | INFO. The adapters will add their own data as well. diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index a2ebf55335..b385bdbe83 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + API Documentation Guidelines ============================ diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index c19c8e0bec..52ea605a72 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Asset Pipeline ================== @@ -147,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. @@ -167,9 +169,8 @@ directory. Files in this directory are served by the Sprockets middleware. Assets can still be placed in the `public` hierarchy. Any assets under `public` will be served as static files by the application or web server when -`config.serve_static_assets` is set to true. You should use -`app/assets` for files that must undergo some pre-processing before they are -served. +`config.serve_static_files` is set to true. You should use `app/assets` for +files that must undergo some pre-processing before they are served. In production, Rails precompiles these files to `public/assets` by default. The precompiled copies are then served as static assets by the web server. The files @@ -181,12 +182,12 @@ When you generate a scaffold or a controller, Rails also generates a JavaScript file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`) for that controller. Additionally, when generating a scaffold, Rails generates -the file scaffolds.css (or scaffolds.css.scss if `sass-rails` is in the +the file scaffolds.css (or scaffolds.scss if `sass-rails` is in the `Gemfile`.) For example, if you generate a `ProjectsController`, Rails will also add a new -file at `app/assets/javascripts/projects.js.coffee` and another at -`app/assets/stylesheets/projects.css.scss`. By default these files will be ready +file at `app/assets/javascripts/projects.coffee` and another at +`app/assets/stylesheets/projects.scss`. By default these files will be ready to use by your application immediately using the `require_tree` directive. See [Manifest Files and Directives](#manifest-files-and-directives) for more details on require_tree. @@ -231,7 +232,9 @@ images, JavaScript files or stylesheets. scope of the application or those libraries which are shared across applications. * `vendor/assets` is for assets that are owned by outside entities, such as -code for JavaScript plugins and CSS frameworks. +code for JavaScript plugins and CSS frameworks. Keep in mind that third party +code with references to other files also processed by the asset Pipeline (images, +stylesheets, etc.), will need to be rewritten to use helpers like `asset_path`. WARNING: If you are upgrading from Rails 3, please take into account that assets under `lib/assets` or `vendor/assets` are available for inclusion via the @@ -421,7 +424,7 @@ $('#logo').attr({ src: "<%= asset_path('logo.png') %>" }); This writes the path to the particular asset being referenced. Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb` -extension (e.g., `application.js.coffee.erb`): +extension (e.g., `application.coffee.erb`): ```js $('#logo').attr src: "<%= asset_path('logo.png') %>" @@ -522,8 +525,8 @@ The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which -generated an `app/assets/javascripts/projects.js.coffee` and an -`app/assets/stylesheets/projects.css.scss` file. +generated an `app/assets/javascripts/projects.coffee` and an +`app/assets/stylesheets/projects.scss` file. In development mode, or if the asset pipeline is disabled, when these files are requested they are processed by the processors provided by the `coffee-script` @@ -535,13 +538,13 @@ web server. Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet -called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, +called `app/assets/stylesheets/projects.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file - -`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then +`app/assets/javascripts/projects.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript. Keep in mind the order of these preprocessors is important. For example, if -you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` +you called your JavaScript file `app/assets/javascripts/projects.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems. @@ -1347,8 +1350,8 @@ config.assets.digest = true Rails 4 no longer sets default config values for Sprockets in `test.rb`, so `test.rb` now requires Sprockets configuration. The old defaults in the test -environment are: `config.assets.compile = true`, `config.assets.compress = -false`, `config.assets.debug = false` and `config.assets.digest = false`. +environment are: `config.assets.compile = true`, `config.assets.compress = false`, +`config.assets.debug = false` and `config.assets.digest = false`. The following should also be added to `Gemfile`: diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index c9e0fcd939..d6b2e75e1e 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Associations ========================== @@ -101,13 +103,13 @@ class CreateOrders < ActiveRecord::Migration def change create_table :customers do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :orders do |t| t.belongs_to :customer, index: true t.datetime :order_date - t.timestamps + t.timestamps null: false end end end @@ -132,13 +134,13 @@ class CreateSuppliers < ActiveRecord::Migration def change create_table :suppliers do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :accounts do |t| t.belongs_to :supplier, index: true t.string :account_number - t.timestamps + t.timestamps null: false end end end @@ -165,13 +167,13 @@ class CreateCustomers < ActiveRecord::Migration def change create_table :customers do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :orders do |t| - t.belongs_to :customer, index:true + t.belongs_to :customer, index: true t.datetime :order_date - t.timestamps + t.timestamps null: false end end end @@ -207,19 +209,19 @@ class CreateAppointments < ActiveRecord::Migration def change create_table :physicians do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :patients do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :appointments do |t| t.belongs_to :physician, index: true t.belongs_to :patient, index: true t.datetime :appointment_date - t.timestamps + t.timestamps null: false end end end @@ -291,19 +293,19 @@ class CreateAccountHistories < ActiveRecord::Migration def change create_table :suppliers do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :accounts do |t| t.belongs_to :supplier, index: true t.string :account_number - t.timestamps + t.timestamps null: false end create_table :account_histories do |t| t.belongs_to :account, index: true t.integer :credit_rating - t.timestamps + t.timestamps null: false end end end @@ -332,12 +334,12 @@ class CreateAssembliesAndParts < ActiveRecord::Migration def change create_table :assemblies do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :parts do |t| t.string :part_number - t.timestamps + t.timestamps null: false end create_table :assemblies_parts, id: false do |t| @@ -371,13 +373,13 @@ class CreateSuppliers < ActiveRecord::Migration def change create_table :suppliers do |t| t.string :name - t.timestamps + t.timestamps null: false end create_table :accounts do |t| t.integer :supplier_id t.string :account_number - t.timestamps + t.timestamps null: false end add_index :accounts, :supplier_id @@ -455,7 +457,7 @@ class CreatePictures < ActiveRecord::Migration t.string :name t.integer :imageable_id t.string :imageable_type - t.timestamps + t.timestamps null: false end add_index :pictures, :imageable_id @@ -471,7 +473,7 @@ class CreatePictures < ActiveRecord::Migration create_table :pictures do |t| t.string :name t.references :imageable, polymorphic: true, index: true - t.timestamps + t.timestamps null: false end end end @@ -501,7 +503,7 @@ class CreateEmployees < ActiveRecord::Migration def change create_table :employees do |t| t.references :manager, index: true - t.timestamps + t.timestamps null: false end end end @@ -689,7 +691,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 @@ -724,10 +726,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 ------------------------------ @@ -756,7 +758,7 @@ class Order < ActiveRecord::Base end ``` -Each instance of the order model will have these methods: +Each instance of the `Order` model will have these methods: ```ruby customer @@ -831,6 +833,7 @@ The `belongs_to` association supports these options: * `:polymorphic` * `:touch` * `:validate` +* `:optional` ##### `:autosave` @@ -879,10 +882,12 @@ class Order < ActiveRecord::Base belongs_to :customer, counter_cache: :count_of_orders end class Customer < ActiveRecord::Base - has_many :orders + has_many :orders, counter_cache: :count_of_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. + Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. ##### `:dependent` @@ -952,6 +957,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: @@ -1342,16 +1352,16 @@ class Customer < ActiveRecord::Base end ``` -Each instance of the customer model will have these methods: +Each instance of the `Customer` model will have these methods: ```ruby orders(force_reload = false) orders<<(object, ...) orders.delete(object, ...) orders.destroy(object, ...) -orders=objects +orders=(objects) order_ids -order_ids=ids +order_ids=(ids) orders.clear orders.empty? orders.size @@ -1495,6 +1505,7 @@ The `has_many` association supports these options: * `:as` * `:autosave` * `:class_name` +* `:counter_cache` * `:dependent` * `:foreign_key` * `:inverse_of` @@ -1522,6 +1533,10 @@ class Customer < ActiveRecord::Base end ``` +##### `:counter_cache` + +This option can be used to configure a custom named `:counter_cache`. You only need this option when you customized the name of your `:counter_cache` on the [belongs_to association](#options-for-belongs-to). + ##### `:dependent` Controls what happens to the associated objects when their owner is destroyed: @@ -1532,8 +1547,6 @@ Controls what happens to the associated objects when their owner is destroyed: * `:restrict_with_exception` causes an exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects -NOTE: This option is ignored when you use the `:through` option on the association. - ##### `:foreign_key` By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: @@ -1831,16 +1844,16 @@ class Part < ActiveRecord::Base end ``` -Each instance of the part model will have these methods: +Each instance of the `Part` model will have these methods: ```ruby assemblies(force_reload = false) assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) -assemblies=objects +assemblies=(objects) assembly_ids -assembly_ids=ids +assembly_ids=(ids) assemblies.clear assemblies.empty? assemblies.size @@ -1979,8 +1992,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 ``` @@ -1992,7 +2005,6 @@ The `has_and_belongs_to_many` association supports these options: * `:foreign_key` * `:join_table` * `:validate` -* `:readonly` ##### `:association_foreign_key` @@ -2236,3 +2248,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 new file mode 100644 index 0000000000..9e78eebf82 --- /dev/null +++ b/guides/source/autoloading_and_reloading_constants.md @@ -0,0 +1,1313 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Autoloading and Reloading Constants +=================================== + +This guide documents how constant autoloading and reloading works. + +After reading this guide, you will know: + +* Key aspects of Ruby constants +* What is `autoload_paths` +* How constant autoloading works +* What is `require_dependency` +* How constant reloading works +* Solutions to common autoloading gotchas + +-------------------------------------------------------------------------------- + + +Introduction +------------ + +Ruby on Rails allows applications to be written as if their code was preloaded. + +In a normal Ruby program classes need to load their dependencies: + +```ruby +require 'application_controller' +require 'post' + +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +Our Rubyist instinct quickly sees some redundancy in there: If classes were +defined in files matching their name, couldn't their loading be automated +somehow? We could save scanning the file for dependencies, which is brittle. + +Moreover, `Kernel#require` loads files once, but development is much more smooth +if code gets refreshed when it changes without restarting the server. It would +be nice to be able to use `Kernel#load` in development, and `Kernel#require` in +production. + +Indeed, those features are provided by Ruby on Rails, where we just write + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +This guide documents how that works. + + +Constants Refresher +------------------- + +While constants are trivial in most programming languages, they are a rich +topic in Ruby. + +It is beyond the scope of this guide to document Ruby constants, but we are +nevertheless going to highlight a few key topics. Truly grasping the following +sections is instrumental to understanding constant autoloading and reloading. + +### Nesting + +Class and module definitions can be nested to create namespaces: + +```ruby +module XML + class SAXParser + # (1) + end +end +``` + +The *nesting* at any given place is the collection of enclosing nested class and +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 +[XML::SAXParser, XML] +``` + +It is important to understand that the nesting is composed of class and module +*objects*, it has nothing to do with the constants used to access them, and is +also unrelated to their names. + +For instance, while this definition is similar to the previous one: + +```ruby +class XML::SAXParser + # (2) +end +``` + +the nesting in (2) is different: + +```ruby +[XML::SAXParser] +``` + +`XML` does not belong to it. + +We can see in this example that the name of a class or module that belongs to a +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) + end +end +``` + +The nesting in (3) consists of two module objects: + +```ruby +[A::B, X::Y] +``` + +So, it not only doesn't end in `A`, which does not even belong to the nesting, +but it also contains `X::Y`, which is independent from `A::B`. + +The nesting is an internal stack maintained by the interpreter, and it gets +modified according to these rules: + +* The class object following a `class` keyword gets pushed when its body is +executed, and popped after it. + +* The module object following a `module` keyword gets pushed when its body is +executed, and popped after it. + +* A singleton class opened with `class << object` gets pushed, and popped later. + +* 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. 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 +a newly created anonymous module is pushed by Ruby. + +It is interesting to observe that blocks do not modify the stack. In particular +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. + +### Class and Module Definitions are Constant Assignments + +Let's suppose the following snippet creates a class (rather than reopening it): + +```ruby +class C +end +``` + +Ruby creates a constant `C` in `Object` and stores in that constant a class +object. The name of the class instance is "C", a string, named after the +constant. + +That is, + +```ruby +class Project < ActiveRecord::Base +end +``` + +performs a constant assignment equivalent to + +```ruby +Project = Class.new(ActiveRecord::Base) +``` + +including setting the name of the class as a side-effect: + +```ruby +Project.name # => "Project" +``` + +Constant assignment has a special rule to make that happen: if the object +being assigned is an anonymous class or module, Ruby sets the object's name to +the name of the constant. + +INFO. From then on, what happens to the constant and the instance does not +matter. For example, the constant could be deleted, the class object could be +assigned to a different constant, be stored in no constant anymore, etc. Once +the name is set, it doesn't change. + +Similarly, module creation using the `module` keyword as in + +```ruby +module Admin +end +``` + +performs a constant assignment equivalent to + +```ruby +Admin = Module.new +``` + +including setting the name as a side-effect: + +```ruby +Admin.name # => "Admin" +``` + +WARNING. The execution context of a block passed to `Class.new` or `Module.new` +is not entirely equivalent to the one of the body of the definitions using the +`class` and `module` keywords. But both idioms result in the same constant +assignment. + +Thus, when one informally says "the `String` class", that really means: the +class object stored in the constant called "String" in the class object stored +in the `Object` constant. `String` is otherwise an ordinary Ruby constant and +everything related to constants such as resolution algorithms applies to it. + +Likewise, in the controller + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +`Post` is not syntax for a class. Rather, `Post` is a regular Ruby constant. If +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. + +### Constants are Stored in Modules + +Constants belong to modules in a very literal sense. Classes and modules have +a constant table; think of it as a hash table. + +Let's analyze an example to really understand what that means. While common +abuses of language like "the `String` class" are convenient, the exposition is +going to be precise here for didactic purposes. + +Let's consider the following module definition: + +```ruby +module Colors + RED = '0xff0000' +end +``` + +First, when the `module` keyword is processed, the interpreter creates a new +entry in the constant table of the class object stored in the `Object` constant. +Said entry associates the name "Colors" to a newly created module object. +Furthermore, the interpreter sets the name of the new module object to be the +string "Colors". + +Later, when the body of the module definition is interpreted, a new entry is +created in the constant table of the module object stored in the `Colors` +constant. That entry maps the name "RED" to the string "0xff0000". + +In particular, `Colors::RED` is totally unrelated to any other `RED` constant +that may live in any other class or module object. If there were any, they +would have separate entries in their respective constant tables. + +Pay special attention in the previous paragraphs to the distinction between +class and module objects, constant names, and value objects associated to them +in constant tables. + +### Resolution Algorithms + +#### Resolution Algorithm for Relative Constants + +At any given place in the code, let's define *cref* to be the first element of +the nesting if it is not empty, or `Object` otherwise. + +Without getting too much into the details, the resolution algorithm for relative +constant references goes like this: + +1. If the nesting is not empty the constant is looked up in its elements and in +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 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 +the name of the constant to be autoloaded, and the cref. See more in [Relative +References](#autoloading-algorithms-relative-references). + +#### Resolution Algorithm for Qualified Constants + +Qualified constants look like this: + +```ruby +Billing::Invoice +``` + +`Billing::Invoice` is composed of two constants: `Billing` is relative and is +resolved using the algorithm of the previous section. + +INFO. Leading colons would make the first segment absolute rather than +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 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: + +1. The constant is looked up in the parent and its ancestors. + +2. If the lookup fails, `const_missing` is invoked in the parent. The default +implementation of `const_missing` raises `NameError`, but it can be overridden. + +As you see, this algorithm is simpler than the one for relative constants. In +particular, the nesting plays no role here, and modules are not special-cased, +if neither they nor their ancestors have the constants, `Object` is **not** +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](#autoloading-algorithms-qualified-references). + + +Vocabulary +---------- + +### Parent Namespaces + +Given a string with a constant path we define its *parent namespace* to be the +string that results from removing its rightmost segment. + +For example, the parent namespace of the string "A::B::C" is the string "A::B", +the parent namespace of "A::B" is "A", and the parent namespace of "A" is "". + +The interpretation of a parent namespace when thinking about classes and modules +is tricky though. Let's consider a module M named "A::B": + +* The parent namespace, "A", may not reflect nesting at a given spot. + +* The constant `A` may no longer exist, some code could have removed it from +`Object`. + +* If `A` exists, the class or module that was originally in `A` may not be there +anymore. For example, if after a constant removal there was another constant +assignment there would generally be a different object in there. + +* In such case, it could even happen that the reassigned `A` held a new class or +module called also "A"! + +* In the previous scenarios M would no longer be reachable through `A::B` but +the module object itself could still be alive somewhere and its name would +still be "A::B". + +The idea of a parent namespace is at the core of the autoloading algorithms +and helps explain and understand their motivation intuitively, but as you see +that metaphor leaks easily. Given an edge case to reason about, take always into +account that by "parent namespace" the guide means exactly that specific string +derivation. + +### Loading Mechanism + +Rails autoloads files with `Kernel#load` when `config.cache_classes` is false, +the default in development mode, and with `Kernel#require` otherwise, the +default in production mode. + +`Kernel#load` allows Rails to execute files more than once if [constant +reloading](#constant-reloading) is enabled. + +This guide uses the word "load" freely to mean a given file is interpreted, but +the actual mechanism can be `Kernel#load` or `Kernel#require` depending on that +flag. + + +Autoloading Availability +------------------------ + +Rails is always able to autoload provided its environment is in place. For +example the `runner` command autoloads: + +``` +$ bin/rails runner 'p User.column_names' +["id", "email", "created_at", "updated_at"] +``` + +The console autoloads, the test suite autoloads, and of course the application +autoloads. + +By default, Rails eager loads the application files when it boots in production +mode, so most of the autoloading going on in development does not happen. But +autoloading may still be triggered during eager loading. + +For example, given + +```ruby +class BeachHouse < House +end +``` + +if `House` is still unknown when `app/models/beach_house.rb` is being eager +loaded, Rails autoloads it. + + +autoload_paths +-------------- + +As you probably know, when `require` gets a relative file name: + +```ruby +require 'erb' +``` + +Ruby looks for the file in the directories listed in `$LOAD_PATH`. That is, Ruby +iterates over all its directories and for each one of them checks whether they +have a file called "erb.rb", or "erb.so", or "erb.o", or "erb.dll". If it finds +any of them, the interpreter loads it and ends the search. Otherwise, it tries +again in the next directory of the list. If the list gets exhausted, `LoadError` +is raised. + +We are going to cover how constant autoloading works in more detail later, but +the idea is that when a constant like `Post` is hit and missing, if there's a +`post.rb` file for example in `app/models` Rails is going to find it, evaluate +it, and have `Post` defined as a side-effect. + +Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which +to look up `post.rb`. That collection is called `autoload_paths` and by +default it contains: + +* All subdirectories of `app` in the application and engines. For example, + `app/controllers`. They do not need to be the default ones, any custom + directories like `app/workers` belong automatically to `autoload_paths`. + +* Any existing second level directories called `app/*/concerns` in the + application and engines. + +* The directory `test/mailers/previews`. + +Also, this collection is configurable via `config.autoload_paths`. For example, +`lib` was in the list years ago, but no longer is. An application can opt-in +by adding this to `config/application.rb`: + +```ruby +config.autoload_paths << "#{Rails.root}/lib" +``` +`config.autoload_paths` is accessible from environment-specific configuration files, but any changes made to it outside `config/application.rb` don't have an effect. + +The value of `autoload_paths` can be inspected. In a just generated application +it is (edited): + +``` +$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths' +.../app/assets +.../app/controllers +.../app/helpers +.../app/mailers +.../app/models +.../app/controllers/concerns +.../app/models/concerns +.../test/mailers/previews +``` + +INFO. `autoload_paths` is computed and cached during the initialization process. +The application needs to be restarted to reflect any changes in the directory +structure. + + +Autoloading Algorithms +---------------------- + +### Relative References + +A relative constant reference may appear in several places, for example, in + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +all three constant references are relative. + +#### Constants after the `class` and `module` Keywords + +Ruby performs a lookup for the constant that follows a `class` or `module` +keyword because it needs to know if the class or module is going to be created +or reopened. + +If the constant is not defined at that point it is not considered to be a +missing constant, autoloading is **not** triggered. + +So, in the previous example, if `PostsController` is not defined when the file +is interpreted Rails autoloading is not going to be triggered, Ruby will just +define the controller. + +#### Top-Level Constants + +On the contrary, if `ApplicationController` is unknown, the constant is +considered missing and an autoload is going to be attempted by Rails. + +In order to load `ApplicationController`, Rails iterates over `autoload_paths`. +First checks if `app/assets/application_controller.rb` exists. If it does not, +which is normally the case, it continues and finds +`app/controllers/application_controller.rb`. + +If the file defines the constant `ApplicationController` all is fine, otherwise +`LoadError` is raised: + +``` +unable to autoload constant ApplicationController, expected +<full path to application_controller.rb> to define it (LoadError) +``` + +INFO. Rails does not require the value of autoloaded constants to be a class or +module object. For example, if the file `app/models/max_clients.rb` defines +`MAX_CLIENTS = 100` autoloading `MAX_CLIENTS` works just fine. + +#### Namespaces + +Autoloading `ApplicationController` looks directly under the directories of +`autoload_paths` because the nesting in that spot is empty. The situation of +`Post` is different, the nesting in that line is `[PostsController]` and support +for namespaces comes into play. + +The basic idea is that given + +```ruby +module Admin + class BaseController < ApplicationController + @@all_roles = Role.all + end +end +``` + +to autoload `Role` we are going to check if it is defined in the current or +parent namespaces, one at a time. So, conceptually we want to try to autoload +any of + +``` +Admin::BaseController::Role +Admin::Role +Role +``` + +in that order. That's the idea. To do so, Rails looks in `autoload_paths` +respectively for file names like these: + +``` +admin/base_controller/role.rb +admin/role.rb +role.rb +``` + +modulus some additional directory lookups we are going to cover soon. + +INFO. `'Constant::Name'.underscore` gives the relative path without extension of +the file name where `Constant::Name` is expected to be defined. + +Let's see how Rails autoloads the `Post` constant in the `PostsController` +above assuming the application has a `Post` model defined in +`app/models/post.rb`. + +First it checks for `posts_controller/post.rb` in `autoload_paths`: + +``` +app/assets/posts_controller/post.rb +app/controllers/posts_controller/post.rb +app/helpers/posts_controller/post.rb +... +test/mailers/previews/posts_controller/post.rb +``` + +Since the lookup is exhausted without success, a similar search for a directory +is performed, we are going to see why in the [next section](#automatic-modules): + +``` +app/assets/posts_controller/post +app/controllers/posts_controller/post +app/helpers/posts_controller/post +... +test/mailers/previews/posts_controller/post +``` + +If all those attempts fail, then Rails starts the lookup again in the parent +namespace. In this case only the top-level remains: + +``` +app/assets/post.rb +app/controllers/post.rb +app/helpers/post.rb +app/mailers/post.rb +app/models/post.rb +``` + +A matching file is found in `app/models/post.rb`. The lookup stops there and the +file is loaded. If the file actually defines `Post` all is fine, otherwise +`LoadError` is raised. + +### Qualified References + +When a qualified constant is missing Rails does not look for it in the parent +namespaces. But there is a caveat: When a constant is missing, Rails is +unable to tell if the trigger was a relative reference or a qualified one. + +For example, consider + +```ruby +module Admin + User +end +``` + +and + +```ruby +Admin::User +``` + +If `User` is missing, in either case all Rails knows is that a constant called +"User" was missing in a module called "Admin". + +If there is a top-level `User` Ruby would resolve it in the former example, but +wouldn't in the latter. In general, Rails does not emulate the Ruby constant +resolution algorithms, but in this case it tries using the following heuristic: + +> If none of the parent namespaces of the class or module has the missing +> constant then Rails assumes the reference is relative. Otherwise qualified. + +For example, if this code triggers autoloading + +```ruby +Admin::User +``` + +and the `User` constant is already present in `Object`, it is not possible that +the situation is + +```ruby +module Admin + User +end +``` + +because otherwise Ruby would have resolved `User` and no autoloading would have +been triggered in the first place. Thus, Rails assumes a qualified reference and +considers the file `admin/user.rb` and directory `admin/user` to be the only +valid options. + +In practice, this works quite well as long as the nesting matches all parent +namespaces respectively and the constants that make the rule apply are known at +that time. + +However, autoloading happens on demand. If by chance the top-level `User` was +not yet loaded, then Rails assumes a relative reference by contract. + +Naming conflicts of this kind are rare in practice, but if one occurs, +`require_dependency` provides a solution by ensuring that the constant needed +to trigger the heuristic is defined in the conflicting place. + +### Automatic Modules + +When a module acts as a namespace, Rails does not require the application to +defines a file for it, a directory matching the namespace is enough. + +Suppose an application has a back office whose controllers are stored in +`app/controllers/admin`. If the `Admin` module is not yet loaded when +`Admin::UsersController` is hit, Rails needs first to autoload the constant +`Admin`. + +If `autoload_paths` has a file called `admin.rb` Rails is going to load that +one, but if there's no such file and a directory called `admin` is found, Rails +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 +[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*). + +The procedure to autoload constant `C` in an arbitrary situation is as follows: + +``` +if the class or module in which C is missing is Object + let ns = '' +else + let M = the class or module in which C is missing + + if M is anonymous + let ns = '' + else + let ns = M.name + end +end + +loop do + # Look for a regular file. + for dir in autoload_paths + if the file "#{dir}/#{ns.underscore}/c.rb" exists + load/require "#{dir}/#{ns.underscore}/c.rb" + + if C is now defined + return + else + raise LoadError + end + end + end + + # Look for an automatic module. + for dir in autoload_paths + if the directory "#{dir}/#{ns.underscore}/c" exists + if ns is an empty string + let C = Module.new in Object and return + else + let C = Module.new in ns.constantize and return + end + end + end + + if ns is empty + # We reached the top-level without finding the constant. + raise NameError + else + if C exists in any of the parent namespaces + # Qualified constants heuristic. + raise NameError + else + # Try again in the parent namespace. + let ns = the parent namespace of ns and retry + end + end +end +``` + + +require_dependency +------------------ + +Constant autoloading is triggered on demand and therefore code that uses a +certain constant may have it already defined or may trigger an autoload. That +depends on the execution path and it may vary between runs. + +There are times, however, in which you want to make sure a certain constant is +known when the execution reaches some code. `require_dependency` provides a way +to load a file using the current [loading mechanism](#loading-mechanism), and +keeping track of constants defined in that file as if they were autoloaded to +have them reloaded as needed. + +`require_dependency` is rarely needed, but see a couple of use-cases in +[Autoloading and STI](#autoloading-and-sti) and [When Constants aren't +Triggered](#when-constants-aren-t-missed). + +WARNING. Unlike autoloading, `require_dependency` does not expect the file to +define any particular constant. Exploiting this behavior would be a bad practice +though, file and constant paths should match. + + +Constant Reloading +------------------ + +When `config.cache_classes` is false Rails is able to reload autoloaded +constants. + +For example, in you're in a console session and edit some file behind the +scenes, the code can be reloaded with the `reload!` command: + +``` +> reload! +``` + +When the application runs, code is reloaded when something relevant to this +logic changes. In order to do that, Rails monitors a number of things: + +* `config/routes.rb`. + +* Locales. + +* Ruby files under `autoload_paths`. + +* `db/schema.rb` and `db/structure.sql`. + +If anything in there changes, there is a middleware that detects it and reloads +the code. + +Autoloading keeps track of autoloaded constants. Reloading is implemented by +removing them all from their respective classes and modules using +`Module#remove_const`. That way, when the code goes on, those constants are +going to be unknown again, and files reloaded on demand. + +INFO. This is an all-or-nothing operation, Rails does not attempt to reload only +what changed since dependencies between classes makes that really tricky. +Instead, everything is wiped. + + +Module#autoload isn't Involved +------------------------------ + +`Module#autoload` provides a lazy way to load constants that is fully integrated +with the Ruby constant lookup algorithms, dynamic constant API, etc. It is quite +transparent. + +Rails internals make extensive use of it to defer as much work as possible from +the boot process. But constant autoloading in Rails is **not** implemented with +`Module#autoload`. + +One possible implementation based on `Module#autoload` would be to walk the +application tree and issue `autoload` calls that map existing file names to +their conventional constant name. + +There are a number of reasons that prevent Rails from using that implementation. + +For example, `Module#autoload` is only capable of loading files using `require`, +so reloading would not be possible. Not only that, it uses an internal `require` +which is not `Kernel#require`. + +Then, it provides no way to remove declarations in case a file is deleted. If a +constant gets removed with `Module#remove_const` its `autoload` is not triggered +again. Also, it doesn't support qualified names, so files with namespaces should +be interpreted during the walk tree to install their own `autoload` calls, but +those files could have constant references not yet configured. + +An implementation based on `Module#autoload` would be awesome but, as you see, +at least as of today it is not possible. Constant autoloading in Rails is +implemented with `Module#const_missing`, and that's why it has its own contract, +documented in this guide. + + +Common Gotchas +-------------- + +### Nesting and Qualified Constants + +Let's consider + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + +and + +```ruby +class Admin::UsersController < ApplicationController + def index + @users = User.all + end +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)). + +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, +`Admin::User` will get autoloaded in either case. + +Albeit qualified constants with `class` and `module` keywords may technically +work with autoloading in some cases, it is preferable to use relative constants +instead: + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + +### Autoloading and STI + +Single Table Inheritance (STI) is a feature of Active Record that enables +storing a hierarchy of models in one single table. The API of such models is +aware of the hierarchy and encapsulates some common needs. For example, given +these classes: + +```ruby +# app/models/polygon.rb +class Polygon < ActiveRecord::Base +end + +# app/models/triangle.rb +class Triangle < Polygon +end + +# app/models/rectangle.rb +class Rectangle < Polygon +end +``` + +`Triangle.create` creates a row that represents a triangle, and +`Rectangle.create` creates a row that represents a rectangle. If `id` is the +ID of an existing record, `Polygon.find(id)` returns an object of the correct +type. + +Methods that operate on collections are also aware of the hierarchy. For +example, `Polygon.all` returns all the records of the table, because all +rectangles and triangles are polygons. Active Record takes care of returning +instances of their corresponding class in the result set. + +Types are autoloaded as needed. For example, if `Polygon.first` is a rectangle +and `Rectangle` has not yet been loaded, Active Record autoloads it and the +record is correctly instantiated. + +All good, but if instead of performing queries based on the root class we need +to work on some subclass, things get interesting. + +While working with `Polygon` you do not need to be aware of all its descendants, +because anything in the table is by definition a polygon, but when working with +subclasses Active Record needs to be able to enumerate the types it is looking +for. Let’s see an example. + +`Rectangle.all` only loads rectangles by adding a type constraint to the query: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +Let’s introduce now a subclass of `Rectangle`: + +```ruby +# app/models/square.rb +class Square < Rectangle +end +``` + +`Rectangle.all` should now return rectangles **and** squares: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle", "Square") +``` + +But there’s a caveat here: How does Active Record know that the class `Square` +exists at all? + +Even if the file `app/models/square.rb` exists and defines the `Square` class, +if no code yet used that class, `Rectangle.all` issues the query + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +That is not a bug, the query includes all *known* descendants of `Rectangle`. + +A way to ensure this works correctly regardless of the order of execution is to +load the leaves of the tree by hand at the bottom of the file that defines the +root class: + +```ruby +# app/models/polygon.rb +class Polygon < ActiveRecord::Base +end +require_dependency ‘square’ +``` + +Only the leaves that are **at least grandchildren** need to be loaded this +way. Direct subclasses do not need to be preloaded. If the hierarchy is +deeper, intermediate classes will be autoloaded recursively from the bottom +because their constant will appear in the class definitions as superclass. + +### Autoloading and `require` + +Files defining constants to be autoloaded should never be `require`d: + +```ruby +require 'user' # DO NOT DO THIS + +class UsersController < ApplicationController + ... +end +``` + +There are two possible gotchas here in development mode: + +1. If `User` is autoloaded before reaching the `require`, `app/models/user.rb` +runs again because `load` does not update `$LOADED_FEATURES`. + +2. If the `require` runs first Rails does not mark `User` as an autoloaded +constant and changes to `app/models/user.rb` aren't reloaded. + +Just follow the flow and use constant autoloading always, never mix +autoloading and `require`. As a last resort, if some file absolutely needs to +load a certain file use `require_dependency` to play nice with constant +autoloading. This option is rarely needed in practice, though. + +Of course, using `require` in autoloaded files to load ordinary 3rd party +libraries is fine, and Rails is able to distinguish their constants, they are +not marked as autoloaded. + +### Autoloading and Initializers + +Consider this assignment in `config/initializers/set_auth_service.rb`: + +```ruby +AUTH_SERVICE = if Rails.env.production? + RealAuthService +else + MockedAuthService +end +``` + +The purpose of this setup would be that the application uses the class that +corresponds to the environment via `AUTH_SERVICE`. In development mode +`MockedAuthService` gets autoloaded when the initializer runs. Let’s suppose +we do some requests, change its implementation, and hit the application again. +To our surprise the changes are not reflected. Why? + +As [we saw earlier](#constant-reloading), Rails removes autoloaded constants, +but `AUTH_SERVICE` stores the original class object. Stale, non-reachable +using the original constant, but perfectly functional. + +The following code summarizes the situation: + +```ruby +class C + def quack + 'quack!' + end +end + +X = C +Object.instance_eval { remove_const(:C) } +X.new.quack # => quack! +X.name # => C +C # => uninitialized constant C (NameError) +``` + +Because of that, it is not a good idea to autoload constants on application +initialization. + +In the case above we could implement a dynamic access point: + +```ruby +# app/models/auth_service.rb +class AuthService + if Rails.env.production? + def self.instance + RealAuthService + end + else + def self.instance + MockedAuthService + end + end +end +``` + +and have the application use `AuthService.instance` instead. `AuthService` +would be loaded on demand and be autoload-friendly. + +### `require_dependency` and Initializers + +As we saw before, `require_dependency` loads files in an autoloading-friendly +way. Normally, though, such a call does not make sense in an initializer. + +One could think about doing some [`require_dependency`](#require-dependency) +calls in an initializer to make sure certain constants are loaded upfront, for +example as an attempt to address the [gotcha with STIs](#autoloading-and-sti). + +Problem is, in development mode [autoloaded constants are wiped](#constant-reloading) +if there is any relevant change in the file system. If that happens then +we are in the very same situation the initializer wanted to avoid! + +Calls to `require_dependency` have to be strategically written in autoloaded +spots. + +### When Constants aren't Missed + +#### Relative References + +Let's consider a flight simulator. The application has a default flight model + +```ruby +# app/models/flight_model.rb +class FlightModel +end +``` + +that can be overridden by each airplane, for instance + +```ruby +# app/models/bell_x1/flight_model.rb +module BellX1 + class FlightModel < FlightModel + end +end + +# app/models/bell_x1/aircraft.rb +module BellX1 + class Aircraft + def initialize + @flight_model = FlightModel.new + end + end +end +``` + +The initializer wants to create a `BellX1::FlightModel` and nesting has +`BellX1`, that looks good. But if the default flight model is loaded and the +one for the Bell-X1 is not, the interpreter is able to resolve the top-level +`FlightModel` and autoloading is thus not triggered for `BellX1::FlightModel`. + +That code depends on the execution path. + +These kind of ambiguities can often be resolved using qualified constants: + +```ruby +module BellX1 + class Plane + def flight_model + @flight_model ||= BellX1::FlightModel.new + end + end +end +``` + +Also, `require_dependency` is a solution: + +```ruby +require_dependency 'bell_x1/flight_model' + +module BellX1 + class Plane + def flight_model + @flight_model ||= FlightModel.new + end + end +end +``` + +#### Qualified References + +Given + +```ruby +# app/models/hotel.rb +class Hotel +end + +# app/models/image.rb +class Image +end + +# app/models/hotel/image.rb +class Hotel + class Image < Image + end +end +``` + +the expression `Hotel::Image` is ambiguous because it depends on the execution +path. + +As [we saw before](#resolution-algorithm-for-qualified-constants), Ruby looks +up the constant in `Hotel` and its ancestors. If `app/models/image.rb` has +been loaded but `app/models/hotel/image.rb` hasn't, Ruby does not find `Image` +in `Hotel`, but it does in `Object`: + +``` +$ bin/rails r 'Image; p Hotel::Image' 2>/dev/null +Image # NOT Hotel::Image! +``` + +The code evaluating `Hotel::Image` needs to make sure +`app/models/hotel/image.rb` has been loaded, possibly with +`require_dependency`. + +In these cases the interpreter issues a warning though: + +``` +warning: toplevel constant Image referenced by Hotel::Image +``` + +This surprising constant resolution can be observed with any qualifying class: + +``` +2.1.5 :001 > String::Array +(irb):1: warning: toplevel constant Array referenced by String::Array + => Array +``` + +WARNING. To find this gotcha the qualifying namespace has to be a class, +`Object` is not an ancestor of modules. + +### Autoloading within Singleton Classes + +Let's suppose we have these class definitions: + +```ruby +# app/models/hotel/services.rb +module Hotel + class Services + end +end + +# app/models/hotel/geo_location.rb +module Hotel + class GeoLocation + class << self + Services + end + end +end +``` + +If `Hotel::Services` is known by the time `app/models/hotel/geo_location.rb` +is being loaded, `Services` is resolved by Ruby because `Hotel` belongs to the +nesting when the singleton class of `Hotel::GeoLocation` is opened. + +But if `Hotel::Services` is not known, Rails is not able to autoload it, the +application raises `NameError`. + +The reason is that autoloading is triggered for the singleton class, which is +anonymous, and as [we saw before](#generic-procedure), Rails only checks the +top-level namespace in that edge case. + +An easy solution to this caveat is to qualify the constant: + +```ruby +module Hotel + class GeoLocation + class << self + Hotel::Services + end + end +end +``` + +### Autoloading in `BasicObject` + +Direct descendants of `BasicObject` do not have `Object` among their ancestors +and cannot resolve top-level constants: + +```ruby +class C < BasicObject + String # NameError: uninitialized constant C::String +end +``` + +When autoloading is involved that plot has a twist. Let's consider: + +```ruby +class C < BasicObject + def user + User # WRONG + end +end +``` + +Since Rails checks the top-level namespace `User` gets autoloaded just fine the +first time the `user` method is invoked. You only get the exception if the +`User` constant is known at that point, in particular in a *second* call to +`user`: + +```ruby +c = C.new +c.user # surprisingly fine, User +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)). + +As with pure Ruby, within the body of a direct descendant of `BasicObject` use +always absolute constant paths: + +```ruby +class C < BasicObject + ::String # RIGHT + + def user + ::User # RIGHT + end +end +``` diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index cbcd053950..0fa20f7ccf 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Caching with Rails: An overview =============================== @@ -105,7 +107,7 @@ This method generates a cache key that depends on all products and can be used i <% end %> ``` -If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless` +If you want to cache a fragment under certain conditions, you can use `cache_if` or `cache_unless` ```erb <% cache_if (condition, cache_key_for_products) do %> @@ -182,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 ------------ diff --git a/guides/source/command_line.md b/guides/source/command_line.md index a074b849c6..19ccdc5488 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Rails Command Line ====================== @@ -24,7 +26,7 @@ There are a few commands that are absolutely critical to your everyday usage of * `rails dbconsole` * `rails new app_name` -All commands can run with ```-h or --help``` to list more information. +All commands can run with `-h` or `--help` to list more information. Let's create a simple Rails application to step through each of these commands in context. @@ -61,7 +63,7 @@ With no further work, `rails server` will run our new shiny Rails app: $ cd commandsapp $ bin/rails server => Booting WEBrick -=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 +=> Rails 5.0.0 application starting in development on http://localhost:3000 => Call with -d to detach => Ctrl-C to shutdown server [2013-08-07 02:00:01] INFO WEBrick 1.3.1 @@ -79,7 +81,7 @@ The server can be run on a different port using the `-p` option. The default dev $ bin/rails server -e production -p 4000 ``` -The `-b` option binds Rails to the specified IP, by default it is 0.0.0.0. You can run a server as a daemon by passing a `-d` option. +The `-b` option binds Rails to the specified IP, by default it is localhost. You can run a server as a daemon by passing a `-d` option. ### `rails generate` @@ -130,7 +132,7 @@ Example: `rails generate controller CreditCards open debit credit close` Credit card controller with URLs like /credit_cards/debit. - Controller: app/controllers/credit_card_controller.rb + Controller: app/controllers/credit_cards_controller.rb Test: test/controllers/credit_cards_controller_test.rb Views: app/views/credit_cards/debit.html.erb [...] Helper: app/helpers/credit_cards_helper.rb @@ -151,9 +153,9 @@ $ bin/rails generate controller Greetings hello create app/helpers/greetings_helper.rb invoke assets invoke coffee - create app/assets/javascripts/greetings.js.coffee + create app/assets/javascripts/greetings.coffee invoke scss - create app/assets/stylesheets/greetings.css.scss + create app/assets/stylesheets/greetings.scss ``` What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file and a stylesheet file. @@ -239,11 +241,11 @@ $ bin/rails generate scaffold HighScore game:string score:integer create app/views/high_scores/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/high_scores.js.coffee + create app/assets/javascripts/high_scores.coffee invoke scss - create app/assets/stylesheets/high_scores.css.scss + create app/assets/stylesheets/high_scores.scss invoke scss - identical app/assets/stylesheets/scaffolds.css.scss + identical app/assets/stylesheets/scaffolds.scss ``` The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything. @@ -284,7 +286,7 @@ If you wish to test out some code without changing any data, you can do that by ```bash $ bin/rails console --sandbox -Loading development environment in sandbox (Rails 4.2.0) +Loading development environment in sandbox (Rails 5.0.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` @@ -368,8 +370,7 @@ Rake is Ruby Make, a standalone Ruby utility that replaces the Unix utility 'mak You can get a list of Rake tasks available to you, which will often depend on your current directory, by typing `rake --tasks`. Each task has a description, and should help you find the thing you need. -To get the full backtrace for running rake task you can pass the option -```--trace``` to command line, for example ```rake db:create --trace```. +To get the full backtrace for running rake task you can pass the option `--trace` to command line, for example `rake db:create --trace`. ```bash $ bin/rake --tasks @@ -382,10 +383,10 @@ rake db:create # Create the database from config/database.yml for the c rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development) rake middleware # Prints out your Rack middleware stack ... -rake tmp:clear # Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear) -rake tmp:create # Creates tmp directories for sessions, cache, sockets, and pids +rake tmp:clear # Clear cache and socket files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear) +rake tmp:create # Creates tmp directories for cache, sockets, and pids ``` -INFO: You can also use ```rake -T``` to get the list of tasks. +INFO: You can also use `rake -T` to get the list of tasks. ### `about` @@ -394,16 +395,11 @@ INFO: You can also use ```rake -T``` to get the list of tasks. ```bash $ bin/rake about About your application's environment -Ruby version 1.9.3 (x86_64-linux) -RubyGems version 1.3.6 -Rack version 1.3 -Rails version 4.2.0 +Rails version 5.0.0 +Ruby version 2.2.0 (x86_64-linux) +RubyGems version 2.4.5 +Rack version 1.6 JavaScript Runtime Node.js (V8) -Active Record version 4.2.0 -Action Pack version 4.2.0 -Action View version 4.2.0 -Action Mailer version 4.2.0 -Active Support version 4.2.0 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 Application root /home/foobar/commandsapp Environment development @@ -413,10 +409,7 @@ Database schema version 20110805173523 ### `assets` -You can precompile the assets in `app/assets` using `rake assets:precompile`, -and remove older compiled assets using `rake assets:clean`. The `assets:clean` -task allows for rolling deploys that may still be linking to an old asset while -the new assets are being built. +You can precompile the assets in `app/assets` using `rake assets:precompile`, and remove older compiled assets using `rake assets:clean`. The `assets:clean` task allows for rolling deploys that may still be linking to an old asset while the new assets are being built. If you want to clear `public/assets` completely, you can use `rake assets:clobber`. @@ -426,14 +419,6 @@ The most common tasks of the `db:` Rake namespace are `migrate` and `create`, an 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`. - ### `notes` `rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations. @@ -503,15 +488,14 @@ Rails comes with a test suite called Minitest. Rails owes its stability to the u ### `tmp` -The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions. +The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like process id files and cached actions. The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` directory: * `rake tmp:cache:clear` clears `tmp/cache`. -* `rake tmp:sessions:clear` clears `tmp/sessions`. * `rake tmp:sockets:clear` clears `tmp/sockets`. -* `rake tmp:clear` clears all the three: cache, sessions and sockets. -* `rake tmp:create` creates tmp directories for sessions, cache, sockets, and pids. +* `rake tmp:clear` clears all cache and sockets files. +* `rake tmp:create` creates tmp directories for cache, sockets and pids. ### Miscellaneous diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 667281d1aa..4fc1301e06 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Configuring Rails Applications ============================== @@ -62,7 +64,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such * `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. -* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes. Can also be enabled with `threadsafe!`. +* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes. * `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`. @@ -86,7 +88,7 @@ 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. This flag is set to false by `config.threadsafe!`. +* `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. @@ -108,7 +110,7 @@ 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 modes except production, where it defaults to `:info`. +* `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. @@ -120,7 +122,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`. -* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. NGINX or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. +* `config.serve_static_files` configures Rails to serve static files. This option defaults to true, but in the production environment it is set to false because the server software (e.g. NGINX or Apache) used to run the application should serve static files instead. If you are running or testing your app in production mode using WEBrick (it is not recommended to use WEBrick in production) set the option to true. Otherwise, you won't be able to use page caching and request for files that exist under the public directory. * `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified: @@ -153,7 +155,7 @@ pipeline is enabled. It is set to true by default. * `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder. -* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb`. +* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb` and `development.rb`. * `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`. @@ -197,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_assets` is `false`. +* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_files` is `false`. * `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. @@ -214,7 +216,7 @@ Every Rails application comes with a standard set of middleware which it uses in * `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value. * `ActionDispatch::ParamsParser` parses out parameters from the request into `params`. * `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types. -* `ActionDispatch::Head` converts HEAD requests to GET requests and serves them as so. +* `Rack::Head` converts HEAD requests to GET requests and serves them as so. Besides these usual middleware, you can add your own by using the `config.middleware.use` method: @@ -225,13 +227,13 @@ config.middleware.use Magical::Unicorns This will put the `Magical::Unicorns` middleware on the end of the stack. You can use `insert_before` if you wish to add a middleware before another. ```ruby -config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_before Rack::Head, Magical::Unicorns ``` There's also `insert_after` which will insert a middleware after another: ```ruby -config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_after Rack::Head, Magical::Unicorns ``` Middlewares can also be completely swapped out and replaced with others: @@ -298,6 +300,8 @@ 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.belongs_to_required_by_default` is a boolean value and controls whether `belongs_to` association is required by default. + 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. @@ -318,6 +322,8 @@ The schema dumper adds one additional configuration option: * `config.action_controller.default_charset` specifies the default character set for all renders. The default is "utf-8". +* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default is `true`. + * `config.action_controller.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 Controller. Set to `nil` to disable logging. * `config.action_controller.request_forgery_protection_token` sets the token parameter name for RequestForgery. Calling `protect_from_forgery` sets it to `:authenticity_token` by default. @@ -364,6 +370,30 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) for more information. It defaults to true. +* `config.action_dispatch.rescue_responses` configures what exceptions are assigned to an HTTP status. It accepts a hash and you can specify pairs of exception/status. By default, this is defined as: + + ```ruby + config.action_dispatch.rescue_responses = { + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::UnknownHttpMethod' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, + 'ActionDispatch::ParamsParser::ParseError' => :bad_request, + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + } + ``` + + Any exceptions that are not configured will be mapped to 500 Internal Server Error. + * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. * `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. @@ -479,6 +509,8 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. +* `config.active_support.halt_callback_chains_on_return_false` specifies whether ActiveRecord, ActiveModel and ActiveModel::Validations callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`. + * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. * `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations. @@ -662,7 +694,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: @@ -1019,3 +1051,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 analyse 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 8bc4b10591..32d1e2c6e7 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Contributing to Ruby on Rails ============================= @@ -24,7 +26,7 @@ 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 do not find any issue addressing it you may proceed 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. @@ -171,6 +173,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: @@ -193,7 +203,7 @@ Now get busy and add/edit code. You're on your branch now, so you can write what * Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution. -TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. +TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)). #### Follow the Coding Conventions @@ -205,7 +215,7 @@ Rails follows a simple set of coding style conventions: * Use Ruby >= 1.9 syntax for hashes. Prefer `{ a: :b }` over `{ :a => :b }`. * Prefer `&&`/`||` over `and`/`or`. * Prefer class << self over self.method for class methods. -* Use `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. +* Use `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. * Use `a = b` and not `a=b`. * Use assert_not methods instead of refute. * Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks. @@ -287,7 +297,12 @@ $ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout The `-n` option allows you to run a single method instead of the whole file. -##### Testing Active Record +#### Testing Active Record + +First, create the databases you'll need. For MySQL and PostgreSQL, +running the SQL statements `create database activerecord_unittest` and +`create database activerecord_unittest2` is sufficient. This is not +necessary for SQLite3. This is how you run the Active Record test suite only for SQLite3: @@ -361,6 +376,10 @@ A CHANGELOG entry should summarize what was changed and should end with author's 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. +### Updating the Gemfile.lock + +Some changes requires the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. + ### Sanity Check You should not be the only person who looks at the code before you submit it. @@ -559,6 +578,23 @@ $ git push origin my_pull_request -f You should be able to refresh the pull request on GitHub and see that it has been updated. +#### Updating pull request + +Sometimes you will be asked to make some changes to the code you have +already committed. This can include amending existing commits. In this +case Git will not allow you to push the changes as the pushed branch +and local branch do not match. Instead of opening a new pull request, +you can force push to your branch on GitHub as described earlier in +squashing commits section: + +```bash +$ git push origin my_pull_request -f +``` + +This will update the branch and pull request on GitHub with your new code. Do +note that using force push may result in commits being lost on the remote branch; use it with care. + + ### Older Versions of Ruby on Rails If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch: diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 8767fbecce..61ea0b44ef 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -40,7 +40,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi <% end %> <%= author('Tore Darell', 'toretore') do %> - Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. His home on the Internet is his blog <a href="http://tore.darell.no">Sneaky Abstractions</a>. + Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>. <% end %> <%= author('Jeff Dean', 'zilkey') do %> diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 88c6210296..926a048762 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Debugging Rails Applications ============================ @@ -138,7 +140,7 @@ 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. -TIP: The default Rails log level is `info` in production mode and `debug` in development and test mode. +TIP: The default Rails log level is `debug` in all environments. ### Sending Messages @@ -240,6 +242,55 @@ The contents of the block, and therefore the string interpolation, is only evaluated if debug is enabled. This performance savings is only really noticeable with large amounts of logging, but it's a good practice to employ. + +Debugging with the `web-console` gem +------------------------------------- + +The web console allows you to start an interactive Ruby session in your browser. +An interactive console is launched automatically in case of an error but can also +be launched for debugging purposes by invoking `console` in a view or controller. + +For example in a view: + +```ruby +# new.html.erb +<%= console %> +``` + +Or in a controller: + +```ruby +# posts_controller.rb +class PostsController < ApplicationController + def new + console + @post = Post.new + end +end +``` + +### config.web_console.whitelisted_ips + +By default the web console can only be accessed from localhost. +`config.web_console.whitelisted_ips` lets you control which IPs have access to +the console. + +For example, to allow access from both localhost and 192.168.0.100, you can put +inside your configuration file: + +```ruby +config.web_console.whitelisted_ips = %w( 127.0.0.1 192.168.0.100 ) +``` + +Or to allow access from an entire network: + +```ruby +config.web_console.whitelisted_ips = %w( 127.0.0.1 192.168.0.0/16 ) +``` + +The web console is a powerful tool so be careful when you give access to an IP. + + Debugging with the `byebug` gem --------------------------------- @@ -309,7 +360,7 @@ For example: ```bash => Booting WEBrick -=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 +=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) => Ctrl-C to shutdown server @@ -422,11 +473,11 @@ then `backtrace` will supply the answer. --> #0 ArticlesController.index at /PathTo/project/test_app/app/controllers/articles_controller.rb:8 #1 ActionController::ImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4 + at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189 + at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:189 #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:10 ... ``` @@ -438,7 +489,7 @@ context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb +[184, 193] in /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb 184: # is the intended way to override action dispatching. 185: # 186: # Notice that the first argument is the method to be dispatched @@ -542,7 +593,7 @@ This way an irb session will be started 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 let `byebug` help us with it. ``` (byebug) help var @@ -655,7 +706,7 @@ instruction to be executed. In this case, the activesupport's `week` method. ``` (byebug) step -[50, 59] in /PathToGems/activesupport-4.2.0/lib/active_support/core_ext/numeric/time.rb +[50, 59] in /PathToGems/activesupport-5.0.0/lib/active_support/core_ext/numeric/time.rb 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) 51: end 52: alias :day :days @@ -830,7 +881,7 @@ 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 not only runs "EXPLAIN" before each of your select queries in development, but diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 3d9ec578ae..989b29956c 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Development Dependencies Install ================================ diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 5c37837a1c..7ae3640937 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -32,6 +32,11 @@ name: Active Record Query Interface url: active_record_querying.html description: This guide covers the database query interface provided by Active Record. + - + 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 - name: Views documents: @@ -80,9 +85,9 @@ description: This guide provides you with all you need to get started in creating, enqueueing and executing background jobs. - name: Testing Rails Applications - url: testing.html work_in_progress: true - description: This is a rather comprehensive guide to doing both unit and functional tests in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy. + url: testing.html + description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy. - name: Securing Rails Applications url: security.html @@ -108,15 +113,25 @@ url: working_with_javascript_in_rails.html description: This guide covers the built-in Ajax/JavaScript functionality of Rails. - - name: Getting Started with Engines - url: engines.html - description: This guide explains how to write a mountable engine. - work_in_progress: true - - 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 + - + name: Autoloading and Reloading Constants + url: autoloading_and_reloading_constants.html + description: This guide documents how autoloading and reloading constants work. + - + name: Active Support Instrumentation + work_in_progress: true + url: active_support_instrumentation.html + description: This guide explains how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code. + - + name: Profiling Rails Applications + work_in_progress: true + url: profiling.html + description: This guide explains how to profile your Rails applications to improve performance. + - name: Extending Rails documents: @@ -133,6 +148,11 @@ name: Creating and Customizing Rails Generators url: generators.html description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator). + - + name: Getting Started with Engines + url: engines.html + description: This guide explains how to write a mountable engine. + work_in_progress: true - name: Contributing to Ruby on Rails documents: @@ -166,7 +186,6 @@ name: Ruby on Rails 4.2 Release Notes url: 4_2_release_notes.html description: Release notes for Rails 4.2. - work_in_progress: true - name: Ruby on Rails 4.1 Release Notes url: 4_1_release_notes.html diff --git a/guides/source/engines.md b/guides/source/engines.md index 24548a5b01..84017d5e13 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Getting Started with Engines ============================ @@ -32,7 +34,7 @@ directory structure, and are both generated using the `rails plugin new` generator. The difference is that an engine is considered a "full plugin" by Rails (as indicated by the `--full` option that's passed to the generator command). We'll actually be using the `--mountable` option here, which includes -all the features of `--full`, and then some. This guide will refer to these +all the features of `--full`, and then some. This guide will refer to these "full plugins" simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine. @@ -74,13 +76,13 @@ options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal: ```bash -$ bin/rails plugin new blorgh --mountable +$ rails plugin new blorgh --mountable ``` The full list of options for the plugin generator may be seen by typing: ```bash -$ bin/rails plugin --help +$ rails plugin --help ``` The `--mountable` option tells the generator that you want to create a @@ -888,7 +890,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 ``` @@ -1036,31 +1040,42 @@ functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this: ```ruby -get :index +module Blorgh + class FooControllerTest < ActionController::TestCase + def test_index + get :index + ... + end + end +end ``` It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To -do this, you must also pass the `:use_route` option as a parameter on these -requests: +do this, you must set the `@routes` instance variable to the engine's route set +in your setup code: ```ruby -get :index, use_route: :blorgh +module Blorgh + class FooControllerTest < ActionController::TestCase + setup do + @routes = Engine.routes + end + + def test_index + get :index + ... + end + end +end ``` This tells the application that you still want to perform a `GET` request to the `index` action of this controller, but you want to use the engine's route to get there, rather than the application's one. -Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup: - -```ruby -setup do - @routes = Engine.routes -end -``` - -This will also ensure url helpers for the engine will work as expected in your tests. +This also ensures that the engine's URL helpers will work as expected in your +tests. Improving engine functionality ------------------------------ @@ -1155,7 +1170,7 @@ end Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`] -(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). +(http://api.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 9ced77dfd7..90004c611b 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Form Helpers ============ @@ -96,7 +98,15 @@ form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_ ### Helpers for Generating Form Elements -Rails provides a series of helpers for generating form elements such as checkboxes, text fields, and radio buttons. These basic helpers, with names ending in "_tag" (such as `text_field_tag` and `check_box_tag`), generate just a single `<input>` element. The first parameter to these is always the name of the input. When the form is submitted, the name will be passed along with the form data, and will make its way to the `params` hash in the controller with the value entered by the user for that field. For example, if the form contains `<%= text_field_tag(:query) %>`, then you would be able to get the value of this field in the controller with `params[:query]`. +Rails provides a series of helpers for generating form elements such as +checkboxes, text fields, and radio buttons. These basic helpers, with names +ending in `_tag` (such as `text_field_tag` and `check_box_tag`), generate just a +single `<input>` element. The first parameter to these is always the name of the +input. When the form is submitted, the name will be passed along with the form +data, and will make its way to the `params` hash in the controller with the +value entered by the user for that field. For example, if the form contains `<%= +text_field_tag(:query) %>`, then you would be able to get the value of this +field in the controller with `params[:query]`. When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in [chapter 7 of this guide](#understanding-parameter-naming-conventions). For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html). @@ -265,7 +275,7 @@ 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" /> @@ -290,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> @@ -506,6 +516,12 @@ As the name implies, this only generates option tags. To generate a working sele <%= collection_select(:person, :city_id, City.all, :id, :name) %> ``` +As with other helpers, if you were to use the `collection_select` helper on a form builder scoped to the `@person` object, the syntax would be: + +```erb +<%= f.collection_select(:city_id, City.all, :id, :name) %> +``` + To recap, `options_from_collection_for_select` is to `collection_select` what `options_for_select` is to `select`. NOTE: Pairs passed to `options_for_select` should have the name first and the id second, however with `options_from_collection_for_select` the first argument is the value method and the second the text method. @@ -671,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 diff --git a/guides/source/generators.md b/guides/source/generators.md index f5d2c67cb4..14f451cbc9 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Creating and Customizing Rails Generators & Templates ===================================================== @@ -197,11 +199,11 @@ $ bin/rails generate scaffold User name:string create app/views/users/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/users.js.coffee + create app/assets/javascripts/users.coffee invoke scss - create app/assets/stylesheets/users.css.scss + create app/assets/stylesheets/users.scss invoke scss - create app/assets/stylesheets/scaffolds.css.scss + create app/assets/stylesheets/scaffolds.scss ``` Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. @@ -407,7 +409,7 @@ $ bin/rails generate scaffold Comment body:text create app/views/comments/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/comments.js.coffee + create app/assets/javascripts/comments.coffee invoke scss ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 1769448531..51b8a2ca5f 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Getting Started with Rails ========================== @@ -90,18 +92,18 @@ current version of Ruby installed: TIP: A number of tools exist to help you quickly install Ruby and Ruby on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp). +For more installation methods for most Operating Systems take a look at +[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). ```bash $ ruby -v ruby 2.0.0p353 ``` -If you don't have Ruby installed have a look at -[ruby-lang.org](https://www.ruby-lang.org/en/installation/) for possible ways to -install Ruby on your platform. - -Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows -users and others can find installation instructions at [the SQLite3 website](https://www.sqlite.org). +Many popular UNIX-like OSes ship with an acceptable version of SQLite3. +On Windows, if you installed Rails through Rails Installer, you +already have SQLite installed. Others can find installation instructions +at the [SQLite3 website](https://www.sqlite.org). Verify that it is correctly installed and in your PATH: ```bash @@ -123,7 +125,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 4.2.0", you are ready to continue. +If it says something like "Rails 5.0.0", you are ready to continue. ### Creating the Blog Application @@ -165,14 +167,14 @@ of the files and folders that Rails created by default: |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| -|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://bundler.io).| +|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the [Bundler website](http://bundler.io).| |lib/|Extended modules for your application.| |log/|Application log files.| |public/|The only folder seen by the world as-is. Contains static files and compiled assets.| |Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.| |README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| |test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).| -|tmp/|Temporary files (like cache, pid, and session files).| +|tmp/|Temporary files (like cache and pid files).| |vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.| Hello, Rails! @@ -191,12 +193,15 @@ 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. Usually Mac OS X and Windows come with a JavaScript runtime installed. -Rails adds the `therubyracer` gem to the generated `Gemfile` in a -commented line for new apps and you can uncomment if you need it. +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). @@ -259,9 +264,9 @@ invoke helper create app/helpers/welcome_helper.rb invoke assets invoke coffee -create app/assets/javascripts/welcome.js.coffee +create app/assets/javascripts/welcome.coffee invoke scss -create app/assets/stylesheets/welcome.css.scss +create app/assets/stylesheets/welcome.scss ``` Most important of these are of course the controller, located at @@ -300,8 +305,9 @@ Rails.application.routes.draw do # ... ``` -This is your application's _routing file_ which holds entries in a special DSL -(domain-specific language) that tells Rails how to connect incoming requests to +This is your application's _routing file_ which holds entries in a special +[DSL (domain-specific language)](http://en.wikipedia.org/wiki/Domain-specific_language) +that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with `root` and @@ -338,8 +344,8 @@ You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations. Rails provides a `resources` method which can be used to declare a standard REST -resource. Here's what `config/routes.rb` should look like after the -_article resource_ is declared. +resource. You need to add the _article resource_ to the +`config/routes.rb` as follows: ```ruby Rails.application.routes.draw do @@ -422,12 +428,12 @@ If you refresh <http://localhost:3000/articles/new> now, you'll get a new error: This error indicates that Rails cannot find the `new` action inside the `ArticlesController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it -your wanted actions during the generation process. +your desired actions during the generation process. To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/articles_controller.rb` and inside the `ArticlesController` -class, define a `new` method so that the controller now looks like this: +class, define the `new` method so that your controller now looks like this: ```ruby class ArticlesController < ApplicationController @@ -444,23 +450,23 @@ With the `new` method defined in `ArticlesController`, if you refresh You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view -available, Rails errors out. +available, Rails will raise an exception. In the above image, the bottom line has been truncated. Let's see what the full -thing looks like: +error message looks like: >Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" That's quite a lot of text! Let's quickly go through and understand what each -part of it does. +part of it means. -The first part identifies what template is missing. In this case, it's the +The first part identifies which template is missing. In this case, it's the `articles/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for one here because the `ArticlesController` inherits from `ApplicationController`. The next part of the message contains a hash. The `:locale` key in this hash -simply indicates what spoken language template should be retrieved. By default, +simply indicates which spoken language template should be retrieved. By default, this is the English - or "en" - template. The next key, `:formats` specifies the format of template to be served in response. The default format is `:html`, and so Rails is looking for an HTML template. The final key, `:handlers`, is telling @@ -473,14 +479,16 @@ Templates within a basic Rails application like this are kept in a single location, but in more complex applications it could be many different paths. The simplest template that would work in this case would be one located at -`app/views/articles/new.html.erb`. The extension of this file name is key: the -first extension is the _format_ of the template, and the second extension is the -_handler_ that will be used. Rails is attempting to find a template called -`articles/new` within `app/views` for the application. The format for this -template can only be `html` and the handler must be one of `erb`, `builder` or -`coffee`. Because you want to create a new HTML form, you will be using the `ERB` -language. Therefore the file should be called `articles/new.html.erb` and needs -to be located inside the `app/views` directory of the application. +`app/views/articles/new.html.erb`. The extension of this file name is important: +the first extension is the _format_ of the template, and the second extension +is the _handler_ that will be used. Rails is attempting to find a template +called `articles/new` within `app/views` for the application. The format for +this template can only be `html` and the handler must be one of `erb`, +`builder` or `coffee`. Because you want to create a new HTML form, you will be +using the `ERB` language which is designed to embed Ruby in HTML. + +Therefore the file should be called `articles/new.html.erb` and needs to be +located inside the `app/views` directory of the application. Go ahead now and create a new file at `app/views/articles/new.html.erb` and write this content in it: @@ -665,8 +673,8 @@ rake commands to run migrations, and it's possible to undo a migration after it's been applied to your database. Migration filenames include a timestamp to ensure that they're processed in the order that they were created. -If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember, -yours will have a slightly different name), here's what you'll find: +If you look in the `db/migrate/YYYYMMDDHHMMSS_create_articles.rb` file +(remember, yours will have a slightly different name), here's what you'll find: ```ruby class CreateArticles < ActiveRecord::Migration @@ -675,7 +683,7 @@ class CreateArticles < ActiveRecord::Migration t.string :title t.text :text - t.timestamps + t.timestamps null: false end end end @@ -736,7 +744,7 @@ database columns. In the first line we do just that (remember that `@article.save` is responsible for saving the model in the database. Finally, we redirect the user to the `show` action, which we'll define later. -TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter. +TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `app/models/article.rb`. Class names in Ruby must begin with a capital letter. TIP: As we'll see later, `@article.save` returns a boolean indicating whether the article was saved or not. @@ -833,7 +841,7 @@ class ArticlesController < ApplicationController A couple of things to note. We use `Article.find` to find the article we're interested in, passing in `params[:id]` to get the `:id` parameter from the -request. We also use an instance variable (prefixed by `@`) to hold a +request. We also use an instance variable (prefixed with `@`) to hold a reference to the article object. We do this because Rails will pass all instance variables to the view. @@ -903,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> @@ -1266,8 +1275,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: @@ -1279,7 +1288,7 @@ And here's how our app looks so far: Our `edit` page looks very similar to the `new` page; in fact, they both share the same code for displaying the form. Let's remove this duplication by using a view partial. By convention, partial files are -prefixed by an underscore. +prefixed with an underscore. TIP: You can read more about partials in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. @@ -1480,6 +1489,9 @@ Without this file, the confirmation dialog box wouldn'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. @@ -1537,8 +1549,9 @@ class CreateComments < ActiveRecord::Migration # this line adds an integer column called `article_id`. t.references :article, index: true - t.timestamps + t.timestamps null: false end + add_foreign_key :comments, :articles end end ``` @@ -1558,6 +1571,8 @@ run against the current database, so in this case you will just see: == CreateComments: migrating ================================================= -- create_table(:comments) -> 0.0115s +-- add_foreign_key(:comments, :articles) + -> 0.0000s == CreateComments: migrated (0.0119s) ======================================== ``` @@ -1635,8 +1650,8 @@ This creates five files and one empty directory: | app/views/comments/ | Views of the controller are stored here | | test/controllers/comments_controller_test.rb | The test for the controller | | app/helpers/comments_helper.rb | A view helper file | -| app/assets/javascripts/comment.js.coffee | CoffeeScript for the controller | -| app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller | +| app/assets/javascripts/comment.coffee | CoffeeScript for the controller | +| app/assets/stylesheets/comment.scss | Cascading style sheet for the controller | Like with any blog, our readers will create their comments directly after reading the article, and once they have added their comment, will be sent back @@ -1673,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 @@ -1754,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 @@ -1820,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 @@ -1870,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, @@ -2029,28 +2044,17 @@ What's Next? ------------ Now that you've seen your first Rails application, you should feel free to -update it and experiment on your own. But you don't have to do everything -without help. As you need assistance getting up and running with Rails, feel -free to consult these support resources: +update it and experiment on your own. + +Remember you don't have to do everything without help. As you need assistance +getting up and running with Rails, feel free to consult these support +resources: * The [Ruby on Rails Guides](index.html) * The [Ruby on Rails Tutorial](http://railstutorial.org/book) * 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 RedCloth gem. 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 0761d5b39c..4dc4c5a660 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Internationalization (I18n) API ===================================== @@ -28,7 +30,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Ruby [I18n Wiki](http://ruby-i18n.org/wiki) for more information. +NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, also use various gems available to add additional functionality or features. See the [rails-i18n gem](https://github.com/svenfuchs/rails-i18n) for more information. How I18n in Ruby on Rails Works ------------------------------- @@ -101,7 +103,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor 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. -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. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. +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. @@ -262,7 +264,7 @@ get '/:locale' => 'dashboard#index' Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.) -NOTE: Have a look at two plugins which simplify working with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). +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 @@ -288,7 +290,7 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a plugin 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). +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 @@ -484,8 +486,6 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i ``` -Do check the [Rails i18n Wiki](http://rails-i18n.org/wiki) for list of tools available for managing translations. - Overview of the I18n API Features --------------------------------- @@ -530,7 +530,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,6 +588,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. +"Lazy" lookup can also be used in controllers: + +```yaml +en: + books: + create: + success: Book created! +``` + +This is useful for setting flash messages for instance: + +```ruby +class BooksController < ApplicationController + def create + # ... + redirect_to books_url, notice: t('.success') + end +end +``` + ### Interpolation 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. @@ -628,7 +648,7 @@ entry[count == 1 ? 0 : 1] I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero). -If the lookup for the key does not return a Hash suitable for pluralization, an `18n::InvalidPluralizationData` exception is raised. +If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised. ### Setting and Passing a Locale @@ -809,7 +829,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 +849,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 +1030,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 +1047,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 @@ -1052,9 +1073,9 @@ If you find anything missing or wrong in this guide, please file a ticket on our Contributing to Rails I18n -------------------------- -I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. +I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. -Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!)) +Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!)) If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://github.com/guides/pull-requests). @@ -1062,7 +1083,6 @@ If you find your own locale (language) missing from our [example translations da Resources --------- -* [rails-i18n.org](http://rails-i18n.org) - Homepage of the rails-i18n project. You can find lots of useful resources on the [wiki](http://rails-i18n.org/wiki). * [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list. * [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases. * [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 53bf3039fa..8fbb234698 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Rails Initialization Process ================================ @@ -357,7 +359,7 @@ private end def create_tmp_directories - %w(cache pids sessions sockets).each do |dir_to_make| + %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end end @@ -373,13 +375,12 @@ private end ``` -This is where the first output of the Rails initialization happens. This -method creates a trap for `INT` signals, so if you `CTRL-C` the server, -it will exit the process. As we can see from the code here, it will -create the `tmp/cache`, `tmp/pids`, `tmp/sessions` and `tmp/sockets` -directories. It then calls `wrapped_app` which is responsible for -creating the Rack app, before creating and assigning an -instance of `ActiveSupport::Logger`. +This is where the first output of the Rails initialization happens. This method +creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the +process. As we can see from the code here, it will create the `tmp/cache`, +`tmp/pids`, and `tmp/sockets` directories. It then calls `wrapped_app` which is +responsible for creating the Rack app, before creating and assigning an instance +of `ActiveSupport::Logger`. The `super` method will call `Rack::Server.start` which begins its definition like this: diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index a935c51c95..b0e71035c0 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Layouts and Rendering in Rails ============================== @@ -189,7 +191,7 @@ render file: "/u/apps/warehouse_app/current/app/views/products/show" The `:file` option takes an absolute file-system path. Of course, you need to have rights to the view that you're using to render the content. -NOTE: By default, the file is rendered without using the current layout. If you want Rails to put the file into the current layout, you need to add the `layout: true` option. +NOTE: By default, the file is rendered using the current layout. TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to render a file, because Windows filenames do not have the same format as Unix filenames. @@ -253,7 +255,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 @@ -314,12 +316,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 @@ -425,6 +428,18 @@ 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 +(100-199, 204, 205 or 304), it will be dropped from the response. + +##### The `:formats` Option + +Rails uses the format specified in request (or `:html` by default). You can change this adding 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. @@ -940,7 +955,10 @@ You can also specify multiple videos to play by passing an array of videos to th This will produce: ```erb -<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> +<video> + <source src="/videos/trailer.ogg"> + <source src="/videos/movie.ogg"> +</video> ``` #### Linking to Audio Files with the `audio_tag` @@ -1058,6 +1076,42 @@ One way to use partials is to treat them as the equivalent of subroutines: as a 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. +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: + +* `users/index.html.erb` + + ```html+erb + <%= render "shared/search_filters", search: @q do |f| %> + <p> + Name contains: <%= f.text_field :name_contains %> + </p> + <% end %> + ``` + +* `roles/index.html.erb` + + ```html+erb + <%= render "shared/search_filters", search: @q do |f| %> + <p> + Title contains: <%= f.text_field :title_contains %> + </p> + <% end %> + ``` + +* `shared/_search_filters.html.erb` + + ```html+erb + <%= form_for(@q) do |f| %> + <h1>Search form:</h1> + <fieldset> + <%= yield f %> + </fieldset> + <p> + <%= f.submit "Search" %> + </p> + <% end %> + ``` + TIP: For content that is shared among all pages in your application, you can use partials directly from layouts. #### Partial Layouts @@ -1106,6 +1160,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 7f084dd54c..50308f505a 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Maintenance Policy for Ruby on Rails ==================================== @@ -39,7 +41,10 @@ Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. -**Currently included series:** `4.2.Z`, `4.1.Z`. +In special situations, where someone from the Core Team agrees to support more series, +they are included in the list of supported series. + +**Currently included series:** `4.2.Z`, `4.1.Z` (Supported by Rafael França). Security Issues --------------- diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md index f0ee34cfb1..1937369776 100644 --- a/guides/source/nested_model_forms.md +++ b/guides/source/nested_model_forms.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Nested Model Forms ======================== diff --git a/guides/source/plugins.md b/guides/source/plugins.md index dbccfd4021..10738320ef 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Basics of Creating Rails Plugins ==================================== @@ -39,13 +41,13 @@ to run integration tests using a dummy Rails application. Create your plugin with the command: ```bash -$ bin/rails plugin new yaffle +$ rails plugin new yaffle ``` See usage and options by asking for help: ```bash -$ bin/rails plugin new --help +$ rails plugin new --help ``` Testing Your Newly Generated Plugin @@ -433,7 +435,7 @@ Once your README is solid, go through and add rdoc comments to all of the method Once your comments are good to go, navigate to your plugin directory and run: ```bash -$ bin/rake rdoc +$ bundle exec rake rdoc ``` ### References diff --git a/guides/source/profiling.md b/guides/source/profiling.md new file mode 100644 index 0000000000..ce093f78ba --- /dev/null +++ b/guides/source/profiling.md @@ -0,0 +1,16 @@ +*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +A Guide to Profiling Rails Applications +======================================= + +This guide covers built-in mechanisms in Rails for profiling your application. + +After reading this guide, you will know: + +* Rails profiling terminology. +* How to write benchmark tests for your application. +* Other benchmarking approaches and plugins. + +-------------------------------------------------------------------------------- + + diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 6512b14e60..b3e1874048 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Application Templates =========================== diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 01941fa338..993cd5ac44 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails on Rack ============= @@ -61,7 +63,6 @@ Here's how it loads the middlewares: ```ruby def middleware middlewares = [] - middlewares << [Rails::Rack::Debugger] if options[:debugger] middlewares << [::Rack::ContentLength] Hash.new(middlewares) end @@ -99,6 +100,10 @@ To find out more about different `rackup` options: $ rackup --help ``` +### Development and auto-reloading + +Middlewares are loaded once and are not monitored for changes. You will have to restart the server for changes to be reflected in the running application. + Action Dispatcher Middleware Stack ---------------------------------- @@ -229,7 +234,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::Static`** -* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`. +* Used to serve static files. Disabled if `config.serve_static_files` is `false`. **`Rack::Lock`** @@ -249,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`** @@ -273,7 +278,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::Callbacks`** -* Runs the prepare callbacks before serving the request. +* Provides callbacks to be executed before and after dispatching the request. **`ActiveRecord::Migration::CheckPending`** @@ -303,7 +308,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Parses out parameters from the request into `params`. -**`ActionDispatch::Head`** +**`Rack::Head`** * Converts HEAD requests to `GET` requests and serves them as so. diff --git a/guides/source/routing.md b/guides/source/routing.md index b1a287f53a..a689e131ff 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Routing from the Outside In ================================= diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 6206b3c715..1323742488 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails Guides Guidelines =============================== @@ -54,6 +56,7 @@ API Documentation Guidelines The guides and the API should be coherent and consistent where appropriate. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides: * [Wording](api_documentation_guidelines.html#wording) +* [English](api_documentation_guidelines.html#english) * [Example Code](api_documentation_guidelines.html#example-code) * [Filenames](api_documentation_guidelines.html#file-names) * [Fonts](api_documentation_guidelines.html#fonts) diff --git a/guides/source/security.md b/guides/source/security.md index 125dd82666..e486edde31 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails Security Guide ============================ @@ -237,7 +239,7 @@ Or the attacker places the code into the onmouseover event handler of an image: <img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." /> ``` -There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since XmlHttpRequest is subject to the browser Same-Origin policy - meaning only your site can initiate the request. +There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since `XMLHttpRequest` is subject to the browser Same-Origin policy - meaning only your site can initiate the request. To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications: @@ -247,6 +249,15 @@ protect_from_forgery with: :exception This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown. +NOTE: By default, Rails includes jQuery and an [unobtrusive scripting adapter for +jQuery](https://github.com/rails/jquery-ujs), which adds a header called +`X-CSRF-Token` on every non-GET Ajax call made by jQuery with the security token. +Without this header, non-GET Ajax requests won't be accepted by Rails. When using +another library to make Ajax calls, it is necessary to add the security token as +a default header for Ajax calls in your library. To get the token, have a look at +`<meta name='csrf-token' content='THE-TOKEN'>` tag printed by +`<%= csrf_meta_tags %>` in your application view. + It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself: ```ruby @@ -362,7 +373,7 @@ Refer to the Injection section for countermeasures against XSS. It is _recommend **CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface. -A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen. +A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen. Another example changed Google Adsense's e-mail address and password by. If the victim was logged into Google Adsense, the administration interface for Google advertisements campaigns, an attacker could change their credentials.
@@ -942,7 +953,7 @@ unless params[:token].nil? end ``` -When `params[:token]` is one of: `[]`, `[nil]`, `[nil, nil, ...]` or +When `params[:token]` is one of: `[nil]`, `[nil, nil, ...]` or `['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or `IN ('foo', NULL)` where clauses still will be added to the SQL query. @@ -953,9 +964,9 @@ request: | JSON | Parameters | |-----------------------------------|--------------------------| | `{ "person": null }` | `{ :person => nil }` | -| `{ "person": [] }` | `{ :person => nil }` | -| `{ "person": [null] }` | `{ :person => nil }` | -| `{ "person": [null, null, ...] }` | `{ :person => nil }` | +| `{ "person": [] }` | `{ :person => [] }` | +| `{ "person": [null] }` | `{ :person => [] }` | +| `{ "person": [null, null, ...] }` | `{ :person => [] }` | | `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` | It is possible to return to old behaviour and disable `deep_munge` configuring @@ -1018,7 +1029,6 @@ 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) diff --git a/guides/source/testing.md b/guides/source/testing.md index 29724ae011..14bc75aa7d 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + A Guide to Testing Rails Applications ===================================== @@ -29,11 +31,13 @@ Testing support was woven into the Rails fabric from the beginning. It wasn't an By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`. -A dedicated test database allows you to set up and interact with test data in isolation. Tests can mangle test data with confidence, that won't touch the data in the development or production databases. +A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases. + +Also, each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`. ### Rails Sets up for Testing from the Word Go -Rails creates a `test` folder for you as soon as you create a Rails project using `rails new` _application_name_. If you list the contents of this folder then you shall see: +Rails creates a `test` directory for you as soon as you create a Rails project using `rails new` _application_name_. If you list the contents of this directory then you shall see: ```bash $ ls -F test @@ -41,9 +45,9 @@ controllers/ helpers/ mailers/ test_helper.rb fixtures/ integration/ models/ ``` -The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. +The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. There is also a directory for testing your mailers and one for testing view helpers. -Fixtures are a way of organizing test data; they reside in the `fixtures` folder. +Fixtures are a way of organizing test data; they reside in the `fixtures` directory. The `test_helper.rb` file holds the default configuration for your tests. @@ -51,7 +55,7 @@ The `test_helper.rb` file holds the default configuration for your tests. For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures. -You can find comprehensive documentation in the [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). +You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). #### What Are Fixtures? @@ -61,7 +65,7 @@ You'll find fixtures under your `test/fixtures` directory. When you run `rails g #### YAML -YAML-formatted fixtures are a very human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). +YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). Here's a sample YAML fixture file: @@ -78,11 +82,11 @@ 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. Keys which resemble YAML keywords such as 'yes' and 'no' are quoted so that the YAML Parser correctly interprets them. +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. If you are working with [associations](/association_basics.html), you can simply define a reference node between two different fixtures. Here's an example with -a belongs_to/has_many association: +a `belongs_to`/`has_many` association: ```yaml # In fixtures/categories.yml @@ -96,11 +100,9 @@ one: category: about ``` -Note: For associations to reference one another by name, you cannot specify the `id:` - attribute on the fixtures. Rails will auto assign a primary key to be consistent between - runs. If you manually specify an `id:` attribute, this behavior will not work. For more - information on this association behavior please read the - [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). +Notice the `category` key of the `one` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`. + +NOTE: For associations to reference one another by name, you cannot specify the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). #### ERB'in It Up @@ -116,15 +118,17 @@ user_<%= n %>: #### Fixtures in Action -Rails by default automatically loads all fixtures from the `test/fixtures` folder for your models and controllers test. Loading involves three steps: +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 variable 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)) #### Fixtures are Active Record objects -Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically setup as a local variable of the test case. For example: +Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example: ```ruby # this will return the User object for the fixture named david @@ -137,16 +141,35 @@ users(:david).id email(david.girlfriend.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 | + +We will cover each of types Rails tests listed above in this guide. + Unit Testing your Models ------------------------ -In Rails, models tests are what you write to test your models. - -For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. We will be using examples from this generated code and will be supplementing it with additional examples where necessary. +In Rails, unit tests are what you write to test your models. -NOTE: For more information on Rails _scaffolding_, refer to [Getting Started with Rails](getting_started.html) +For this guide we will be using the application we built in the [Getting Started with Rails](getting_started.html) guide. -When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder: +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: ```bash $ bin/rails generate scaffold article title:string body:text @@ -175,18 +198,18 @@ A line by line examination of this file will help get you oriented to Rails test require 'test_helper' ``` -As you know by now, `test_helper.rb` specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests. +By requiring this file, `test_helper.rb` the default configuration to run our tests is loaded. We will include this with all the tests we write, so any methods added to this file are available to all your tests. ```ruby class ArticleTest < ActiveSupport::TestCase ``` -The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. +The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. Later in this guide, you'll see some of the methods it gives you. Any method defined within a class inherited from `Minitest::Test` -(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run. +(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, methods defined as `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run. -Rails adds a `test` method that takes a test name and a block. It generates a normal `Minitest::Unit` test with method names prefixed with `test_`. So, +Rails also adds a `test` method that takes a test name and a block. It generates a normal `Minitest::Unit` test with method names prefixed with `test_`. So you don't have to worry about naming the methods, and you can write something like: ```ruby test "the truth" do @@ -194,7 +217,7 @@ test "the truth" do end ``` -acts as if you had written +Which is approximately the same as writing this: ```ruby def test_the_truth @@ -202,26 +225,37 @@ def test_the_truth end ``` -only the `test` macro allows a more readable test name. You can still use regular method definitions though. +However only the `test` macro allows a more readable test name. You can still use regular method definitions though. + +NOTE: The method name is generated by replacing spaces with underscores. The result does not need to be a valid Ruby identifier though, the name may contain punctuation characters etc. That's because in Ruby technically any string may be a method name. This may require use of `define_method` and `send` calls to function properly, but formally there's little restriction on the name. -NOTE: The method name is generated by replacing spaces with underscores. The result does not need to be a valid Ruby identifier though, the name may contain punctuation characters etc. That's because in Ruby technically any string may be a method name. Odd ones need `define_method` and `send` calls, but formally there's no restriction. +Next, let's look at our first assertion: ```ruby assert true ``` -This line of code is called an _assertion_. An assertion is a line of code that evaluates an object (or expression) for expected results. For example, an assertion can check: +An assertion is a line of code that evaluates an object (or expression) for expected results. For example, an assertion can check: * does this value = that value? * is this object nil? * does this line of code throw an exception? * is the user's password greater than 5 characters? -Every test contains one or more assertions. Only when all the assertions are successful will the test pass. +Every test must contain at least one assertion, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass. ### Maintaining the test database schema -In order to run your tests, your test database will need to have the current structure. The test helper checks whether your test database has any pending migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` into the test database. If migrations are still pending, an error will be raised. +In order to run your tests, your test database will need to have the current +structure. The test helper checks whether your test database has any pending +migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` +into the test database. If migrations are still pending, an error will be +raised. Usually this indicates that your schema is not fully migrated. Running +the migrations against the development database (`bin/rake db:migrate`) will +bring the schema up to date. + +NOTE: If existing migrations required modifications, the test database needs to +be rebuilt. This can be done by executing `bin/rake db:test:prepare`. ### Running Tests @@ -236,6 +270,8 @@ 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 @@ -247,10 +283,10 @@ 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. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch. - The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. +#### Your first failing test + To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case. ```ruby @@ -311,9 +347,13 @@ 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). -TIP: Many Rails developers practice _Test-Driven Development_ (TDD). This is an excellent way to build up a test suite that exercises every part of your application. TDD is beyond the scope of this guide, but one place to start is with [15 TDD steps to create a Rails application](http://andrzejonsoftware.blogspot.com/2007/05/15-tdd-steps-to-create-rails.html). +#### What an error looks like To see how an error gets reported, here's a test containing an error: @@ -343,7 +383,11 @@ 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](http://edgeguides.rubyonrails.org/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 @@ -356,52 +400,26 @@ behavior: $ BACKTRACE=1 bin/rake test test/models/article_test.rb ``` -### What to Include in Your Unit Tests +If we want this test to pass we can modify it to use `assert_raises` like so: + +```ruby +test "should report error" do + # some_undefined_variable is not defined elsewhere in the test case + assert_raises(NameError) do + some_undefined_variable + end +end +``` -Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model. +This test should now pass. ### Available Assertions 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. -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 or descends from `class`.| -| `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 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) +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) 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. @@ -423,10 +441,24 @@ Rails adds some custom assertions of its own to the `minitest` framework: You'll see the usage of some of these assertions in the next chapter. +### A Brief Note About Minitest + +All the basic assertions such as `assert_equal` defined in `Minitest::Assertions` are also available in the classes we use in our own test cases. In fact, Rails provides the following classes for you to inherit from: + +* `ActiveSupport::TestCase` +* `ActionController::TestCase` +* `ActionMailer::TestCase` +* `ActionView::TestCase` +* `ActionDispatch::IntegrationTest` + +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) + Functional Tests for Your Controllers ------------------------------------- -In Rails, testing the various actions of a single controller is called writing functional tests for that controller. Controllers handle the incoming web requests to your application and eventually respond with a rendered view. +In Rails, testing the various actions of a controller is a form of writing functional tests. Remember your controllers handle the incoming web requests to your application and eventually respond with a rendered view. When writing functional tests, you're testing how your actions handle the requests and the expected result, or response in some cases an HTML view. ### What to Include in your Functional Tests @@ -456,21 +488,28 @@ In the `test_should_get_index` test, Rails simulates a request on the action cal 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. @@ -480,7 +519,7 @@ 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)) @@ -500,13 +539,27 @@ If you're familiar with the HTTP protocol, you'll know that `get` is a type of r * `head` * `delete` -All of request types are methods that you can use, however, you'll probably end up using the first two more often than the others. +All of request types have equivalent methods that you can use. In a typical C.R.U.D. application you'll be using `get`, `post`, `put` and `delete` more often. -NOTE: Functional tests do not verify whether the specified request type should be accepted by the action. Request types in this context exist to make your tests more descriptive. +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 responds with no layout" do + get :show, params: { id: articles(:first).id }, xhr: true + + assert_template :index + assert_template layout: nil +end +``` ### The Four Hashes of the Apocalypse -After a request has been made using one of the 6 methods (`get`, `post`, etc.) and processed, you will have 4 Hash objects ready for use: +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. @@ -529,8 +582,8 @@ assigns["something"] assigns(:something) You also have access to three instance variables in your functional tests: * `@controller` - The controller processing the request -* `@request` - The request -* `@response` - The response +* `@request` - The request object +* `@response` - The response object ### Setting Headers and CGI variables @@ -551,6 +604,10 @@ 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: @@ -559,24 +616,22 @@ 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 that you cannot test for template and layout at the same time, with one call to `assert_template` method. -Also, for the `layout` test, you can give a regular expression instead of a string, but using the string, makes -things clearer. On the other hand, you have to include the "layouts" directory name even if you save your layout -file in this standard layout directory. Hence, +NOTE: You cannot test for template and layout at the same time, with a single call to `assert_template`. -```ruby -assert_template layout: "application" -``` +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. -will not work. +#### Asserting Partials -If your view renders any partial, when asserting for the layout, you have to assert for the partial at the same time. +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. -Hence: +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 @@ -585,27 +640,234 @@ test "new should render correct layout" do end ``` -is the correct way to assert for the layout when the view renders a partial with name `_form`. Omitting the `:partial` key in your `assert_template` call will complain. +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 -### A Fuller Functional Test Example +If you remember from earlier one of the Four Hashes of the Apocalypse was `flash`. -Here's another example that uses `flash`, `assert_redirected_to`, and `assert_difference`: +We want to add a `flash` message to our blog application whenever someone +successfully creates a new Article. + +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: 'Hi', body: 'This is my first article.'} + post :create, params: { article: { title: 'Some title' } } end + assert_redirected_to article_path(assigns(:article)) assert_equal 'Article was successfully created.', flash[:notice] end ``` -### Testing Views +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 +Run options: -n test_should_create_article --seed 32266 + +# Running: + +F + +Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s. + + 1) Failure: +ArticlesControllerTest#test_should_create_article [/Users/zzak/code/bench/sharedapp/test/controllers/articles_controller_test.rb:16]: +--- expected ++++ actual +@@ -1 +1 @@ +-"Article was successfully created." ++nil + +1 runs, 4 assertions, 1 failures, 0 errors, 0 skips +``` + +Let's implement the flash message now in our controller. Our `:create` action should now look like this: + +```ruby +def create + @article = Article.new(article_params) + + if @article.save + flash[:notice] = 'Article was successfully created.' + redirect_to @article + else + render 'new' + end +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 +Run options: -n test_should_create_article --seed 18981 + +# Running: + +. + +Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s. + +1 runs, 4 assertions, 0 failures, 0 errors, 0 skips +``` + +### Putting it together -Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax. +At this point our Articles controller tests the `:index` as well as `:new` and `:create` actions. What about dealing with existing data? -NOTE: You may find references to `assert_tag` in other documentation. This has been removed in 4.2. Use `assert_select` instead. +Let's write a test for the `:show` action: + +```ruby +test "should show article" do + article = articles(:one) + get :show, params: { id: article.id } + assert_response :success +end +``` + +Remember from our discussion earlier on fixtures the `articles()` method will give us access to our Articles fixtures. + +How about deleting an existing Article? + +```ruby +test "should destroy article" do + article = articles(:one) + assert_difference('Article.count', -1) do + delete :destroy, params: { id: article.id } + end + + assert_redirected_to articles_path +end +``` + +We can also add a test for updating an existing Article. + +```ruby +test "should update article" do + article = articles(:one) + patch :update, params: { id: article.id, article: { title: "updated" } } + assert_redirected_to article_path(assigns(:article)) +end +``` + +Notice we're starting to see some duplication in these three tests, they both access the same Article fixture data. We can D.R.Y. this up by using the `setup` and `teardown` methods provided by `ActiveSupport::Callbacks`. + +Our test should now look something like this, disregard the other tests we're leaving them out for brevity. + +```ruby +require 'test_helper' + +class ArticlesControllerTest < ActionController::TestCase + # called before every single test + def setup + @article = articles(:one) + end + + # called after every single test + def teardown + # 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, params: { id: @article.id } + assert_response :success + end + + test "should destroy article" do + assert_difference('Article.count', -1) do + delete :destroy, params: { id: @article.id } + end + + assert_redirected_to articles_path + end + + test "should update article" do + patch :update, params: { id: @article.id, article: { title: "updated" } } + assert_redirected_to article_path(assigns(: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 + assert_equal users(:david), assigns(:user) + end +end +``` + +Testing Routes +-------------- + +Like everything else in your Rails application, it is recommended that you test your routes. Below are example tests for the routes of default `show` and `create` action of `Articles` controller above and it should look like: + +```ruby +class ArticleRoutesTest < ActionController::TestCase + test "should route to article" do + assert_routing '/articles/1', { controller: "articles", action: "show", id: "1" } + end + + test "should route to create article" do + assert_routing({ method: 'post', path: '/articles' }, { controller: "articles", action: "create" }) + end +end +``` + +I've added this file here `test/controllers/articles_routes_test.rb` and if we run the test we should see: + +```bash +$ bin/rake test test/controllers/articles_routes_test.rb + +# Running: + +.. + +Finished in 0.069381s, 28.8263 runs/s, 86.4790 assertions/s. + +2 runs, 6 assertions, 0 failures, 0 errors, 0 skips +``` + +For more information on routing assertions available in Rails, see the API documentation for [`ActionDispatch::Assertions::RoutingAssertions`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html). + +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. There are two forms of `assert_select`: @@ -619,7 +881,10 @@ For example, you could verify the contents on the title element in your response assert_select 'title', "Welcome to Rails Testing Guide" ``` -You can also use nested `assert_select` blocks. In this case the inner `assert_select` runs the assertion on the complete collection of elements selected by the outer `assert_select` block: +You can also use nested `assert_select` blocks for deeper investigation. + +In the following example, the inner `assert_select` for `li.menu_item` runs +within the collection of elements selected by the outer block: ```ruby assert_select 'ul.navigation' do @@ -627,7 +892,9 @@ assert_select 'ul.navigation' do end ``` -Alternatively the collection of elements selected by the outer `assert_select` may be iterated through so that `assert_select` may be called separately for each element. Suppose for example that the response contains two ordered lists, each with four list elements then the following tests will both pass. +A collection of selected elements may be iterated through so that `assert_select` may be called separately for each element. + +For example if the response contains two ordered lists, each with four nested list elements then the following tests will both pass. ```ruby assert_select "ol" do |elements| @@ -641,7 +908,7 @@ assert_select "ol" do end ``` -The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb). +This assertion is quite powerful. For more advanced usage, refer to its [documentation](http://www.rubydoc.info/github/rails/rails-dom-testing). #### Additional View-Based Assertions @@ -661,12 +928,45 @@ assert_select_email do end ``` +Testing helpers +--------------- + +In order to test helpers, all you need to do is check that the output of the +helper method matches what you'd expect. Tests related to the helpers are +located under the `test/helpers` directory. + +A helper test looks like so: + +```ruby +require 'test_helper' + +class UserHelperTest < ActionView::TestCase +end +``` + +A helper is just a simple module where you can define methods which are +available into your views. To test the output of the helper's methods, you just +have to use a mixin like this: + +```ruby +class UserHelperTest < ActionView::TestCase + include UserHelper + + test "should return the user name" do + # ... + end +end +``` + +Moreover, since the test class extends from `ActionView::TestCase`, you have +access to Rails' helper methods such as `link_to` or `pluralize`. + Integration Testing ------------------- -Integration tests are used to test the interaction among any number of controllers. They are generally used to test important work flows within your application. +Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application. -Unlike Unit and Functional tests, integration tests have to be explicitly created under the 'test/integration' folder within your application. Rails provides a generator to create an integration test skeleton for you. +For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you. ```bash $ bin/rails generate integration_test user_flows @@ -686,228 +986,94 @@ class UserFlowsTest < ActionDispatch::IntegrationTest end ``` -Integration tests inherit from `ActionDispatch::IntegrationTest`. This makes available some additional helpers to use in your integration tests. Also you need to explicitly include the fixtures to be made available to the test. +Inheriting from `ActionDispatch::IntegrationTest` comes with some advantages. This makes available some additional helpers to use in your integration tests. ### Helpers Available for Integration Tests -In addition to the standard testing helpers, there are some additional helpers available to integration tests: +In addition to the standard testing helpers, inheriting `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's briefly introduce you to the three categories of helpers you get to choose from. -| Helper | Purpose | -| ------------------------------------------------------------------ | ------- | -| `https?` | Returns `true` if the session is mimicking a secure HTTPS request.| -| `https!` | Allows you to mimic a secure HTTPS request.| -| `host!` | Allows you to set the host name to use in the next request.| -| `redirect?` | Returns `true` if the last request was a redirect.| -| `follow_redirect!` | Follows a single redirect response.| -| `request_via_redirect(http_method, path, [parameters], [headers])` | Allows you to make an HTTP request and follow any subsequent redirects.| -| `post_via_redirect(path, [parameters], [headers])` | Allows you to make an HTTP POST request and follow any subsequent redirects.| -| `get_via_redirect(path, [parameters], [headers])` | Allows you to make an HTTP GET request and follow any subsequent redirects.| -| `patch_via_redirect(path, [parameters], [headers])` | Allows you to make an HTTP PATCH request and follow any subsequent redirects.| -| `put_via_redirect(path, [parameters], [headers])` | Allows you to make an HTTP PUT request and follow any subsequent redirects.| -| `delete_via_redirect(path, [parameters], [headers])` | Allows you to make an HTTP DELETE request and follow any subsequent redirects.| -| `open_session` | Opens a new session instance.| +For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html). -### Integration Testing Examples +When performing requests, you will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for your use. -A simple integration test that exercises multiple controllers: +If you'd like to modify the session, or state of your integration test you should look for [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help. -```ruby -require 'test_helper' +### Implementing an integration test -class UserFlowsTest < ActionDispatch::IntegrationTest - test "login and browse site" do - # login via https - https! - get "/login" - assert_response :success +Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly. - post_via_redirect "/login", username: users(:david).username, password: users(:david).password - assert_equal '/welcome', path - assert_equal 'Welcome david!', flash[:notice] +We'll start by generating our integration test skeleton: - https!(false) - get "/articles/all" - assert_response :success - assert assigns(:products) - end -end +```bash +$ bin/rails generate integration_test blog_flow ``` -As you can see the integration test involves multiple controllers and exercises the entire stack from database to dispatcher. In addition you can have multiple session instances open simultaneously in a test and extend those instances with assertion methods to create a very powerful testing DSL (domain-specific language) just for your application. +It should have created a test file placeholder for us, with the output of the previous command you should see: + +```bash + invoke test_unit + create test/integration/blog_flow_test.rb +``` -Here's an example of multiple sessions and custom DSL in an integration test +Now let's open that file and write our first assertion: ```ruby require 'test_helper' -class UserFlowsTest < ActionDispatch::IntegrationTest - test "login and browse site" do - # User david logs in - david = login(:david) - # User guest logs in - guest = login(:guest) - - # Both are now available in different sessions - assert_equal 'Welcome david!', david.flash[:notice] - assert_equal 'Welcome guest!', guest.flash[:notice] - - # User david can browse site - david.browses_site - # User guest can browse site as well - guest.browses_site - - # Continue with other assertions +class BlogFlowTest < ActionDispatch::IntegrationTest + test "can see the welcome page" do + get "/" + assert_select "h1", "Welcome#index" end - - private - - module CustomDsl - def browses_site - get "/products/all" - assert_response :success - assert assigns(:products) - end - end - - def login(user) - open_session do |sess| - sess.extend(CustomDsl) - u = users(user) - sess.https! - sess.post "/login", username: u.username, password: u.password - assert_equal '/welcome', sess.path - sess.https!(false) - end - end end ``` -Rake Tasks for Running your Tests ---------------------------------- - -You don't need to set up and run your tests by hand on a test-by-test basis. -Rails comes with a number of commands to help in testing. -The table below lists all commands that come along in the default Rakefile -when you initiate a Rails project. - -| Tasks | Description | -| ----------------------- | ----------- | -| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as Rails will run all the 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: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:all` | Runs all tests quickly by merging all types and not resetting db | -| `rake test:all:db` | Runs all tests quickly by merging all types and resetting db | - +If you remember from earlier in the "Testing Views" section we covered `assert_select` to query the resulting HTML of a request. -Brief Note About `Minitest` ------------------------------ +When visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass. -Ruby ships with a vast Standard Library for all common use-cases including testing. Since version 1.9, Ruby provides `Minitest`, a framework for testing. All the basic assertions such as `assert_equal` discussed above are actually defined in `Minitest::Assertions`. The classes `ActiveSupport::TestCase`, `ActionController::TestCase`, `ActionMailer::TestCase`, `ActionView::TestCase` and `ActionDispatch::IntegrationTest` - which we have been inheriting in our test classes - include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. +#### Creating articles integration -NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html) - -Setup and Teardown ------------------- - -If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Articles` controller: +How about testing our ability to create a new article in our blog and see the resulting article. ```ruby -require 'test_helper' - -class ArticlesControllerTest < ActionController::TestCase - - # called before every single test - def setup - @article = articles(:one) - end - - # 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 - end - - test "should show article" do - get :show, id: @article.id - assert_response :success - end - - test "should destroy article" do - assert_difference('Article.count', -1) do - delete :destroy, id: @article.id - end - - assert_redirected_to articles_path - end - +test "can create an article" do + get "/articles/new" + assert_response :success + assert_template "articles/new", partial: "articles/_form" + + post "/articles", + params: { article: { title: "can create", body: "article successfully." } } + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "articles/show" + assert_select "p", "Title:\n can create" end ``` -Above, the `setup` method is called before each test and so `@article` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using: +Let's break this test down so we can understand it. -* a block -* a method (like in the earlier example) -* a method name as a symbol -* a lambda +We start by calling the `:new` action on our Articles controller. This response should be successful, and we can verify the correct template is rendered including the form partial. -Let's see the earlier example by specifying `setup` callback by specifying a method name as a symbol: +After this we make a post request to the `:create` action of our Articles controller: ```ruby -require 'test_helper' - -class ArticlesControllerTest < ActionController::TestCase - - # called before every single test - setup :initialize_article - - # called after every single test - def teardown - @article = nil - end - - test "should show article" do - get :show, id: @article.id - assert_response :success - end - - test "should update article" do - patch :update, id: @article.id, article: {} - assert_redirected_to article_path(assigns(:article)) - end - - test "should destroy article" do - assert_difference('Article.count', -1) do - delete :destroy, id: @article.id - end - - assert_redirected_to articles_path - end +post "/articles", + params: { article: { title: "can create", body: "article successfully." } } +assert_response :redirect +follow_redirect! +``` - private +The two lines following the request are to handle the redirect we setup when creating a new article. - def initialize_article - @article = articles(:one) - end -end -``` +NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made. -Testing Routes --------------- +Finally we can assert that our response was successful, template was rendered, and our new article is readable on the page. -Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Articles` controller above should look like: +#### Taking it further -```ruby -test "should route to article" do - assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"} -end -``` +We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editting comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. Testing Your Mailers -------------------- @@ -948,9 +1114,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 @@ -998,7 +1165,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 @@ -1009,39 +1176,58 @@ class UserControllerTest < ActionController::TestCase end ``` -Testing helpers ---------------- +Testing Jobs +------------ -In order to test helpers, all you need to do is check that the output of the -helper method matches what you'd expect. Tests related to the helpers are -located under the `test/helpers` directory. +Since your custom jobs can be queued at different levels inside your application, +you'll need to test both jobs themselves (their behavior when they get enqueued) +and that other entities correctly enqueue them. -A helper test looks like so: +### A Basic Test Case + +By default, when you generate a job, an associated test will be generated as well +under the `test/jobs` directory. Here's an example test with a billing job: ```ruby require 'test_helper' -class UserHelperTest < ActionView::TestCase +class BillingJobTest < ActiveJob::TestCase + test 'that account is charged' do + BillingJob.perform_now(account, product) + assert account.reload.charged_for?(product) + end end ``` -A helper is just a simple module where you can define methods which are -available into your views. To test the output of the helper's methods, you just -have to use a mixin like this: +This test is pretty simple and only asserts that the job get the work done +as expected. + +By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that +your jobs are performed inline. It will also ensure that all previously performed +and enqueued jobs are cleared before any test run so you can safely assume that +no jobs have already been executed in the scope of each test. + +### Custom Assertions And Testing Jobs Inside Other Components + +Active Job ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActiveJob::TestHelper`](http://api.rubyonrails.org/classes/ActiveJob/TestHelper.html). + +It's a good practice to ensure that your jobs correctly get enqueued or performed +wherever you invoke them (e.g. inside your controllers). This is precisely where +the custom assertions provided by Active Job are pretty useful. For instance, +within a model: ```ruby -class UserHelperTest < ActionView::TestCase - include UserHelper +require 'test_helper' - test "should return the user name" do - # ... +class ProductTest < ActiveJob::TestCase + test 'billing job scheduling' do + assert_enqueued_with(job: BillingJob) do + product.charge(account) + end end end ``` -Moreover, since the test class extends from `ActionView::TestCase`, you have -access to Rails' helper methods such as `link_to` or `pluralize`. - Other Testing Approaches ------------------------ @@ -1053,3 +1239,4 @@ The built-in `minitest` based testing is not the only way to test Rails applicat * [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests. * [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions. * [RSpec](http://relishapp.com/rspec), a behavior-driven development framework +* [Capybara](http://jnicklas.github.com/capybara/), Acceptance test framework for web applications diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 2c6797d1fd..20b90bdba0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + A Guide for Upgrading Ruby on Rails =================================== @@ -18,9 +20,10 @@ 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 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. -* Rails 3.2.x is the last branch to support Ruby 1.8.7. +* Rails 5 requires Ruby 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. TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing. @@ -28,7 +31,7 @@ TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterp Rails provides the `rails:update` rake task. After updating the Rails version in the Gemfile, run this rake task. -This will help you with the creation of new files and changes of old files in a +This will help you with the creation of new files and changes of old files in an interactive session. ```bash @@ -47,23 +50,72 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] Don't forget to review the difference, to see if there were any unexpected changes. -Upgrading from Rails 4.1 to Rails 4.2 +Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- -NOTE: This section is a work in progress, please help to improve this by sending -a [pull request](https://github.com/rails/rails/edit/master/guides/source/upgrading_ruby_on_rails.md). +### Halting callback chains by returning `false` -### Web Console +In Rails 4.2, when a 'before' callback returns `false` in ActiveRecord, +ActiveModel and ActiveModel::Validations, then the entire callback chain +is halted. In other words, successive 'before' callbacks are not executed, +and neither is the action wrapped in callbacks. + +In Rails 5.0, returning `false` in a callback will not have this side effect +of halting the callback chain. Instead, callback chains must be explicitly +halted by calling `throw(:abort)`. + +When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in a callback +will still halt the callback chain, but you will receive a deprecation warning +about this upcoming change. -First, add `gem 'web-console', '~> 2.0.0.beta3'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment. +When you are ready, you can opt into the new behavior and remove the deprecation +warning by adding the following configuration to your `config/application.rb`: -Additionally, you can tell Rails to automatically mount a VT100-compatible console on a predetermined path by setting `config.web_console.automount = true` in your `config/environments/development.rb`. You can specify the path by setting `config.web_console.default_mount_path` (note that this defaults to `/console`). + config.active_support.halt_callback_chains_on_return_false = false -TODO: Update `web-console` version to release version. +See [#17227](https://github.com/rails/rails/pull/17227) for more details. + +Upgrading from Rails 4.1 to Rails 4.2 +------------------------------------- + +### Web Console + +First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment. ### Responders -TODO: mention https://github.com/rails/rails/pull/16526 +`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies: + +```ruby +# app/controllers/users_controller.rb + +class UsersController < ApplicationController + respond_to :html, :json + + def show + @user = User.find(params[:id]) + respond_with @user + end +end +``` + +Instance-level `respond_to` is unaffected and does not require the additional gem: + +```ruby +# app/controllers/users_controller.rb + +class UsersController < ApplicationController + def show + @user = User.find(params[:id]) + respond_to do |format| + format.html + format.json { render json: @user } + end + end +end +``` + +See [#16526](https://github.com/rails/rails/pull/16526) for more details. ### Error handling in transaction callbacks @@ -75,7 +127,7 @@ Record callbacks. When you define a `after_rollback` or `after_commit` callback, you will receive a deprecation warning about this upcoming change. When -you are ready, you can opt into the new behvaior and remove the +you are ready, you can opt into the new behavior and remove the deprecation warning by adding following configuration to your `config/application.rb`: @@ -109,6 +161,18 @@ assigning `nil` to a serialized attribute will save it to the database as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"` when using the `JSON` coder). +### Production log level + +In Rails 5, the default log level for the production environment will be changed +to `:debug` (from `:info`). To preserve the current default, add the following +line to your `production.rb`: + +```ruby +# Set to `:info` to match the current default, or set to `:debug` to opt-into +# the future default. +config.log_level = :info +``` + ### `after_bundle` in Rails templates If you have a Rails template that adds all the files in version control, it @@ -141,24 +205,15 @@ after_bundle do end ``` -### Rails Html Sanitizer +### Rails HTML Sanitizer There's a new choice for sanitizing HTML fragments in your applications. The venerable html-scanner approach is now officially being deprecated in favor of -[`Rails Html Sanitizer`](https://github.com/rails/rails-html-sanitizer). +[`Rails HTML Sanitizer`](https://github.com/rails/rails-html-sanitizer). This means the methods `sanitize`, `sanitize_css`, `strip_tags` and `strip_links` are backed by a new implementation. -In the next major Rails version `Rails Html Sanitizer` will be the default -sanitizer. It already is for new applications. - -Include this in your Gemfile to try it out today: - -```ruby -gem 'rails-html-sanitizer' -``` - This new sanitizer uses [Loofah](https://github.com/flavorjones/loofah) internally. Loofah in turn uses Nokogiri, which wraps XML parsers written in both C and Java, so sanitization should be faster no matter which Ruby version you run. @@ -173,9 +228,69 @@ Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more The documentation for `PermitScrubber` and `TargetScrubber` explains how you can gain complete control over when and how elements should be stripped. +If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile: + +```ruby +gem 'rails-deprecated_sanitizer' +``` + ### Rails DOM Testing -TODO: Mention https://github.com/rails/rails/commit/4e97d7585a2f4788b9eed98c6cdaf4bb6f2cf5ce +The [`TagAssertions` module](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing). + + +### Masked Authenticity Tokens + +In order to mitigate SSL attacks, `form_authenticity_token` is now masked so that it varies with each request. Thus, tokens are validated by unmasking and then decrypting. As a result, any strategies for verifying requests from non-rails forms that relied on a static session CSRF token have to take this into account. + +### Action Mailer + +Previously, calling a mailer method on a mailer class will result in the +corresponding instance method being executed directly. With the introduction of +Active Job and `#deliver_later`, this is no longer true. In Rails 4.2, the +invocation of the instance methods are deferred until either `deliver_now` or +`deliver_later` is called. For example: + +```ruby +class Notifier < ActionMailer::Base + def notify(user, ...) + puts "Called" + mail(to: user.email, ...) + end +end + +mail = Notifier.notify(user, ...) # Notifier#notify is not yet called at this point +mail = mail.deliver_now # Prints "Called" +``` + +This should not result in any noticeable differences for most applications. +However, if you need some non-mailer methods to be executed synchronously, and +you were previously relying on the synchronous proxying behavior, you should +define them as class methods on the mailer class directly: + +```ruby +class Notifier < ActionMailer::Base + def self.broadcast_notifications(users, ...) + users.each { |user| Notifier.notify(user, ...) } + end +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 it's 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 ------------------------------------- @@ -200,7 +315,7 @@ will now trigger CSRF protection. Switch to xhr :get, :index, format: :js ``` -to explicitly test an XmlHttpRequest. +to explicitly test an `XmlHttpRequest`. If you really mean to load JavaScript from remote `<script>` tags, skip CSRF protection on that action. @@ -385,7 +500,7 @@ class ReadOnlyModel < ActiveRecord::Base end ``` -This behaviour was never intentionally supported. Due to a change in the internals +This behavior was never intentionally supported. Due to a change in the internals of `ActiveSupport::Callbacks`, this is no longer allowed in Rails 4.1. Using a `return` statement in an inline callback block causes a `LocalJumpError` to be raised when the callback is executed. @@ -435,7 +550,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 @@ -555,7 +670,7 @@ response body, you should be using `render :plain` as most browsers will escape unsafe content in the response for you. We will be deprecating the use of `render :text` in a future version. So please -start using the more precise `:plain:`, `:html`, and `:body` options instead. +start using the more precise `:plain`, `:html`, and `:body` options instead. Using `render :text` may pose a security risk, as the content is sent as `text/html`. @@ -695,7 +810,7 @@ file (in `config/application.rb`): ```ruby # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. -Bundler.require(:default, Rails.env) +Bundler.require(*Rails.groups) ``` ### vendor/plugins @@ -734,7 +849,7 @@ this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. * Rails 4.0 has deprecated `ActiveRecord::TestCase` in favor of `ActiveSupport::TestCase`. * Rails 4.0 has deprecated the old-style hash based finder API. This means that - methods which previously accepted "finder options" no longer do. + methods which previously accepted "finder options" no longer do. For example, `Book.find(:all, conditions: { name: '1984' })` has been deprecated in favor of `Book.where(name: '1984')` * All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's how you can handle the changes: @@ -759,7 +874,7 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur * Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`. Now when confirmation validations fail, the error will be attached to `:#{attribute}_confirmation` instead of `attribute`. -* Rails 4.0 has changed `ActiveModel::Serializers::JSON.include_root_in_json` default value to `false`. Now, Active Model Serializers and Active Record objects have the same default behaviour. This means that you can comment or remove the following option in the `config/initializers/wrap_parameters.rb` file: +* Rails 4.0 has changed `ActiveModel::Serializers::JSON.include_root_in_json` default value to `false`. Now, Active Model Serializers and Active Record objects have the same default behavior. This means that you can comment or remove the following option in the `config/initializers/wrap_parameters.rb` file: ```ruby # Disable root element in JSON by default. @@ -885,7 +1000,7 @@ The order in which helpers from more than one directory are loaded has changed i ### Active Record Observer and Action Controller Sweeper -Active Record Observer and Action Controller Sweeper have been extracted to the `rails-observers` gem. You will need to add the `rails-observers` gem if you require these features. +`ActiveRecord::Observer` and `ActionController::Caching::Sweeper` have been extracted to the `rails-observers` gem. You will need to add the `rails-observers` gem if you require these features. ### sprockets-rails @@ -914,7 +1029,7 @@ The following changes are meant for upgrading your application to the latest Make the following changes to your `Gemfile`. ```ruby -gem 'rails', '3.2.18' +gem 'rails', '3.2.21' group :assets do gem 'sass-rails', '~> 3.2.6' @@ -1039,7 +1154,7 @@ You can help test performance with these additions to your test environment: ```ruby # Configure static asset server for tests with Cache-Control for performance -config.serve_static_assets = true +config.serve_static_files = true config.static_cache_control = 'public, max-age=3600' ``` diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 7c3fd9f69d..e3856a285a 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Working with JavaScript in Rails ================================ @@ -189,6 +191,34 @@ $(document).ready -> Obviously, you'll want to be a bit more sophisticated than that, but it's a start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax). +Another possibility is returning javascript directly from the server side on +remote calls: + +```ruby +# articles_controller +def create + respond_to do |format| + if @article.save + format.html { ... } + format.js do + render js: <<-endjs + alert('Article saved successfully!'); + window.location = '#{article_path(@article)}'; + endjs + end + else + format.html { ... } + format.js do + render js: "alert('There are empty fields in the form!');" + end + end + end +end +``` + +NOTE: If javascript is disabled in the user browser, `format.html { ... }` +block should be executed as fallback. + ### form_tag [`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag) @@ -355,7 +385,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/rails.gemspec b/rails.gemspec index a304b16f57..1398169922 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' 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/**/*'] + s.files = ['README.md'] s.add_dependency 'activesupport', version s.add_dependency 'actionpack', version @@ -28,5 +28,5 @@ Gem::Specification.new do |s| s.add_dependency 'railties', version s.add_dependency 'bundler', '>= 1.3.0', '< 2.0' - s.add_dependency 'sprockets-rails', '~> 3.0.0.beta1' + s.add_dependency 'sprockets-rails' end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 7d6521b2a8..8306233dcd 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,161 +1,115 @@ -* Fix a bug in the `gem` method for Rails templates when non-String options - are used. +* Set Rails console to use log formatter and log level as specified for the + given environment. - Fixes #16709. + Fixes #15470. - *Yves Senn* + *Jacob Evelyn* -* The [web-console](https://github.com/rails/web-console) gem is now - installed by default for new applications. It can help you debug - development exceptions by spawning an interactive console in its cause - binding. +* Add `config/initializers/active_record_belongs_to_required_by_default.rb` - *Ryan Dao*, *Genadi Samokovarov*, *Guillermo Iguaran* + 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_requred_by_default` + to `true` when ActiveRecord is not skipped. -* Add a `required` option to the model generator for associations + As a result, new Rails apps require `belongs_to` association on model + to be valid. - *Sean Griffin* - -* Add `after_bundle` callbacks in Rails templates. Useful for allowing the - generated binstubs to be added to version control. - - Fixes #16292. - - *Stefan Kanev* - -* Pull in the custom configuration concept from dhh/custom_configuration, which allows you to - configure your own code through the Rails configuration object with custom configuration: - - # config/environments/production.rb - config.x.payment_processing.schedule = :daily - config.x.payment_processing.retries = 3 - config.x.super_debugger = true + This initializer is *not* added when running `rake rails:update`, so + old apps ported to Rails 5 will work without any change. - These configuration points are then available through the configuration object: + *Josef Šimánek* - Rails.configuration.x.payment_processing.schedule # => :daily - Rails.configuration.x.payment_processing.retries # => 3 - Rails.configuration.x.super_debugger # => true +* `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. - *DHH* + Fixes: #16433. -* Scaffold generator `_form` partial adds `class="field"` for password - confirmation fields. + *Guo Xiang Tan* - *noinkling* +* Newly generated applications get a `README.md` in Markdown. -* Add `Rails::Application.config_for` to load a configuration for the current - environment. + *Xavier Noria* - # config/exception_notification.yml: - production: - url: http://127.0.0.1:8080 - namespace: my_app_production - development: - url: http://localhost:3001 - namespace: my_app_development +* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. - # config/production.rb - MyApp::Application.configure do - config.middleware.use ExceptionNotifier, config_for(:exception_notification) - end + *Xavier Noria* - *Rafael Mendonça França*, *DHH* +* Force generated routes to be inserted into routes.rb -* Deprecate `Rails::Rack::LogTailer` without replacement. + *Andrew White* - *Rafael Mendonça França* - -* Add a generic --skip-gems options to generator - - This option is useful if users want to remove some gems like jbuilder, - turbolinks, coffee-rails, etc that don't have specific options on the - generator. - - rails new my_app --skip-gems turbolinks coffee-rails - - *Rafael Mendonça França* +* Don't remove all line endings from routes.rb when revoking scaffold. -* Invalid `bin/rails generate` commands will now show spelling suggestions. + Fixes #15913. - *Richard Schneeman* + *Andrew White* -* Add `bin/setup` script to bootstrap an application. +* Rename `--skip-test-unit` option to `--skip-test` in app generator - *Yves Senn* + *Melanie Gilman* -* Replace double quotes with single quotes while adding an entry into Gemfile. +* Add the `method_source` gem to the default Gemfile for apps - *Alexander Belaev* - -* Default `config.assets.digest` to `true` in development. - - *Dan Kang* - -* Load database configuration from the first `database.yml` available in paths. - - *Pier-Olivier Thibault* + *Sean Griffin* -* Reading name and email from git for plugin gemspec. +* Drop old test locations from `rake stats` + - test/functional + - test/unit - Fixes #9589. + *Ravil Bayramgalin* - *Arun Agrawal*, *Abd ar-Rahman Hamidi*, *Roman Shmatov* +* Update `rake stats` to correctly count declarative tests + as methods in `_test.rb` files. -* Fix `console` and `generators` blocks defined at different environments. + *Ravil Bayramgalin* - Fixes #14748. +* Remove deprecated `test:all` and `test:all:db` tasks. *Rafael Mendonça França* -* Move configuration of asset precompile list and version to an initializer. - - *Matthew Draper* +* Remove deprecated `Rails::Rack::LogTailer`. -* Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3. - - *Dmitrii Golub* - -* Add public API to register new extensions for `rake notes`. + *Rafael Mendonça França* - Example: +* Remove deprecated `RAILS_CACHE` constant. - config.annotations.register_extensions("scss", "sass") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + *Rafael Mendonça França* - *Roberto Miranda* +* Remove deprecated `serve_static_assets` configuration. -* Removed unnecessary `rails application` command. + *Rafael Mendonça França* - *Arun Agrawal* +* Use local variables in `_form.html.erb` partial generated by scaffold. -* Make the `rails:template` rake task load the application's initializers. + *Andrew Kozlov* - Fixes #12133. +* Add `config/initializers/callback_terminator.rb` - *Robin Dupret* + Newly generated Rails apps have a new initializer called + `callback_terminator.rb` which sets the value of the configuration option + `config.active_support.halt_callback_chains_on_return_false` to `false`. -* Introduce `Rails.gem_version` as a convenience method to return - `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform - version comparison. + As a result, new Rails apps do not halt callback chains when a callback + returns `false`; only when they are explicitly halted with `throw(:abort)`. - Example: + The terminator is *not* added when running `rake rails:update`, so returning + `false` will still work on old apps ported to Rails 5, displaying a + deprecation warning to prompt users to update their code to the new syntax. - Rails.version #=> "4.1.2" - Rails.gem_version #=> #<Gem::Version "4.1.2"> + *claudiob* - Rails.version > "4.1.10" #=> false - Rails.gem_version > Gem::Version.new("4.1.10") #=> true - Gem::Requirement.new("~> 4.1.2") =~ Rails.gem_version #=> true +* Generated fixtures won't use the id when generated with references attributes. - *Prem Sichanugrist* + *Pablo Olmos de Aguilera Corradini* -* Avoid namespacing routes inside engines. +* Add `--skip-action-mailer` option to the app generator. - Mountable engines are namespaced by default so the generated routes - were too while they should not. + *claudiob* - Fixes #14079. +* Autoload any second level directories called `app/*/concerns`. - *Yves Senn*, *Carlos Antonio da Silva*, *Robin Dupret* + *Alex Robbin* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/railties/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 2950f05b11..7c2197229d 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/railties/Rakefile b/railties/Rakefile index a899d069b5..9a377ce4ee 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -29,6 +29,7 @@ Rake::TestTask.new('test:regular') do |t| t.pattern = 'test/**/*_test.rb' t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end # Generate GEM ---------------------------------------------------------------------------- diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index e7172e491f..b1f7c29b5a 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -14,7 +14,7 @@ require 'rails/version' require 'active_support/railtie' require 'action_dispatch/railtie' -# For Ruby 1.9, UTF-8 is the default internal and external encoding. +# UTF-8 is the default internal and external encoding. silence_warnings do Encoding.default_external = Encoding::UTF_8 Encoding.default_internal = Encoding::UTF_8 @@ -56,10 +56,18 @@ module Rails application && application.config.root end + # Returns the current Rails environment. + # + # Rails.env # => "development" + # Rails.env.development? # => true + # Rails.env.production? # => false def env @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") end + # Sets the Rails environment. + # + # Rails.env = "staging" # => "staging" def env=(environment) @_env = ActiveSupport::StringInquirer.new(environment) 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/application.rb b/railties/lib/rails/application.rb index 67d5bac700..b11815e013 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -88,6 +88,7 @@ module Rails def inherited(base) super Rails.app_class = base + add_lib_to_load_path!(find_root(base.called_from)) end def instance @@ -98,6 +99,10 @@ module Rails new(initial_variable_values, &block).run_load_hooks! end + def find_root(from) + find_root_with_flag "config.ru", from, Dir.pwd + end + # Makes the +new+ method public. # # Note that Rails::Application inherits from Rails::Engine, which @@ -129,8 +134,6 @@ module Rails # are these actually used? @initial_variable_values = initial_variable_values @block = block - - add_lib_to_load_path! end # Returns true if the application is initialized. @@ -175,7 +178,7 @@ module Rails key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else - ActiveSupport::LegacyKeyGenerator.new(config.secret_token) + ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) end end @@ -217,7 +220,7 @@ module Rails # namespace: my_app_development # # # config/production.rb - # MyApp::Application.configure do + # Rails.application.configure do # config.middleware.use ExceptionNotifier, config_for(:exception_notification) # end def config_for(name) @@ -245,7 +248,7 @@ module Rails super.merge({ "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, - "action_dispatch.secret_token" => config.secret_token, + "action_dispatch.secret_token" => secrets.secret_token, "action_dispatch.secret_key_base" => secrets.secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, @@ -313,8 +316,8 @@ module Rails # are changing config.root inside your application definition or having a custom # Rails application, you will need to add lib to $LOAD_PATH on your own in case # you need to load files in lib/ during the application configuration as well. - def add_lib_to_load_path! #:nodoc: - path = File.join config.root, 'lib' + def self.add_lib_to_load_path!(root) #:nodoc: + path = File.join root, 'lib' if File.exist?(path) && !$LOAD_PATH.include?(path) $LOAD_PATH.unshift(path) end @@ -358,14 +361,28 @@ module Rails end def config #:nodoc: - @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd)) + @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) end def config=(configuration) #:nodoc: @config = configuration end - def secrets #:nodoc: + # Returns secrets added to config/secrets.yml. + # + # Example: + # + # development: + # secret_key_base: 836fa3665997a860728bcb9e9a1e704d427cfc920e79d847d79c8a9a907b9e965defa4154b2b86bdec6930adbe33f21364523a6f6ce363865724549fdfc08553 + # test: + # secret_key_base: 5a37811464e7d378488b0f073e2193b093682e4e21f5d6f3ae0a4e1781e61a351fdc878a843424e81c73fb484a40d23f92c8dafac4870e74ede6e5e174423010 + # production: + # secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + # namespace: my_app_production + # + # +Rails.application.secrets.namespace+ returns +my_app_production+ in the + # production environment. + def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new yaml = config.paths["config/secrets"].first @@ -378,6 +395,8 @@ module Rails # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base + # Fallback to config.secret_token if secrets.secret_token isn't set + secrets.secret_token ||= config.secret_token secrets end @@ -401,16 +420,7 @@ module Rails console do unless ::Kernel.private_method_defined?(:y) - if RUBY_VERSION >= '2.0' - require "psych/y" - else - module ::Kernel - def y(*objects) - puts ::Psych.dump_stream(*objects) - end - private :y - end - end + require "psych/y" end end @@ -418,8 +428,8 @@ module Rails # 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+ + # order here when copying migrations from railties we need them in the same + # order as given by +railties_order+. def migration_railties # :nodoc: ordered_railties.flatten - [self] end @@ -507,8 +517,13 @@ module Rails end def validate_secret_key_config! #:nodoc: - if secrets.secret_key_base.blank? && config.secret_token.blank? - raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" + 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`" + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 786dcee007..dc3ec4274b 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -6,12 +6,12 @@ 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_assets, :ssl_options, :static_cache_control, :session_options, + :serve_static_files, :ssl_options, :static_cache_control, :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x @@ -26,7 +26,7 @@ module Rails @filter_parameters = [] @filter_redirect = [] @helpers_paths = [] - @serve_static_assets = true + @serve_static_files = true @static_cache_control = nil @force_ssl = false @ssl_options = {} @@ -49,21 +49,6 @@ module Rails @secret_token = nil @secret_key_base = nil @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) @@ -118,7 +103,7 @@ module Rails end def log_level - @log_level ||= :debug + @log_level ||= (Rails.env.production? ? :info : :debug) end def colorize_logging diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index a00afe008c..02eea82b0c 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -17,7 +17,7 @@ module Rails middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header - if config.serve_static_assets + if config.serve_static_files middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control end @@ -66,7 +66,11 @@ module Rails end def allow_concurrency? - config.allow_concurrency.nil? ? config.cache_classes : config.allow_concurrency + if config.allow_concurrency.nil? + config.cache_classes && config.eager_load + else + config.allow_concurrency + end end def load_rack_cache 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/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index 737977adf9..cf0a4e128f 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -41,9 +41,7 @@ module Rails end def finalize! - route_sets.each do |routes| - routes.finalize! - end + route_sets.each(&:finalize!) end def revert diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb index 8cc8eb1103..5276eb33c9 100644 --- a/railties/lib/rails/backtrace_cleaner.rb +++ b/railties/lib/rails/backtrace_cleaner.rb @@ -4,12 +4,16 @@ module Rails class BacktraceCleaner < ActiveSupport::BacktraceCleaner APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/ RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/ + EMPTY_STRING = ''.freeze + SLASH = '/'.freeze + DOT_SLASH = './'.freeze def initialize super - add_filter { |line| line.sub("#{Rails.root}/", '') } - add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, '') } - add_filter { |line| line.sub('./', '/') } # for tests + @root = "#{Rails.root}/".freeze + add_filter { |line| line.sub(@root, EMPTY_STRING) } + add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) } + add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests add_gem_filters add_silencer { |line| line !~ APP_DIRS_PATTERN } @@ -21,7 +25,8 @@ module Rails return if gems_paths.empty? gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)} - add_filter { |line| line.sub(gems_regexp, '\2 (\3) \4') } + gems_result = '\2 (\3) \4'.freeze + add_filter { |line| line.sub(gems_regexp, gems_result) } end end end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 0ae6d2a455..0bdf63943f 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -6,9 +6,8 @@ class CodeStatistics #:nodoc: 'Helper tests', 'Model tests', 'Mailer tests', - 'Integration tests', - 'Functional tests (old)', - 'Unit tests (old)'] + 'Job tests', + 'Integration tests'] def initialize(*pairs) @pairs = pairs 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/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index 6cfbc70c51..8bae08e44e 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -127,7 +127,7 @@ EOT require 'rails/generators' require_application_and_environment! Rails.application.load_generators - require "rails/commands/#{command}" + require_command!(command) end # Change to the application's path if there is no config.ru file in current directory. diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 96ced3c2f9..5d37a2b699 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -18,14 +18,6 @@ module Rails 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.on("--debugger", 'Enables the debugger.') do |v| - if RUBY_VERSION < '2.0.0' - options[:debugger] = v - else - puts "=> Notice: debugger option is ignored since Ruby 2.0 and " \ - "it will be removed in future versions." - end - end opt.parse!(arguments) end @@ -76,25 +68,7 @@ module Rails Rails.env = environment end - if RUBY_VERSION < '2.0.0' - def debugger? - options[:debugger] - end - - def require_debugger - require 'debugger' - puts "=> Debugger enabled" - rescue LoadError - puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again." - exit(1) - end - end - def start - if RUBY_VERSION < '2.0.0' - require_debugger if debugger? - end - set_environment! if environment? if sandbox? @@ -105,7 +79,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/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 1a2613a8d0..5175e31f14 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -1,7 +1,6 @@ require 'erb' require 'yaml' require 'optparse' -require 'rbconfig' module Rails class DBConsole @@ -44,7 +43,7 @@ module Rails find_cmd_and_exec(['mysql', 'mysql5'], *args) - when "postgresql", "postgres", "postgis" + when /^postgres|^postgis/ ENV['PGUSER'] = config["username"] if config["username"] ENV['PGHOST'] = config["host"] if config["host"] ENV['PGPORT'] = config["port"].to_s if config["port"] @@ -74,6 +73,21 @@ module Rails find_cmd_and_exec('sqlplus', logon) + when "sqlserver" + args = [] + + args += ["-D", "#{config['database']}"] if config['database'] + args += ["-U", "#{config['username']}"] if config['username'] + args += ["-P", "#{config['password']}"] if config['password'] + + if config['host'] + host_arg = "#{config['host']}" + host_arg << ":#{config['port']}" if config['port'] + args += ["-S", host_arg] + end + + find_cmd_and_exec("sqsh", *args) + else abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" end @@ -157,13 +171,15 @@ module Rails 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/ + unless (ext = RbConfig::CONFIG['EXEEXT']).empty? + commands = commands.map{|cmd| "#{cmd}#{ext}"} + end full_path_command = nil found = commands.detect do |cmd| dirs_on_path.detect do |path| full_path_command = File.join(path, cmd) - File.executable? full_path_command + File.file?(full_path_command) && File.executable?(full_path_command) end end diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb index 5479da86a0..ce26cc3fde 100644 --- a/railties/lib/rails/commands/destroy.rb +++ b/railties/lib/rails/commands/destroy.rb @@ -1,5 +1,7 @@ require 'rails/generators' +#if no argument/-h/--help is passed to rails destroy command, then +#it generates the help associated. if [nil, "-h", "--help"].include?(ARGV.first) Rails::Generators.help 'destroy' exit diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb index 351c59c645..926c36b967 100644 --- a/railties/lib/rails/commands/generate.rb +++ b/railties/lib/rails/commands/generate.rb @@ -1,5 +1,7 @@ require 'rails/generators' +#if no argument/-h/--help is passed to rails generate command, then +#it generates the help associated. if [nil, "-h", "--help"].include?(ARGV.first) Rails::Generators.help 'generate' exit diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb index 95bbdd4cdf..52d8966ead 100644 --- a/railties/lib/rails/commands/plugin.rb +++ b/railties/lib/rails/commands/plugin.rb @@ -11,7 +11,7 @@ else end if File.exist?(railsrc) extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).flat_map {|l| l.split} + extra_args = extra_args_string.split(/\n+/).flat_map(&:split) puts "Using #{extra_args.join(" ")} from #{railsrc}" ARGV.insert(1, *extra_args) 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 c479e92ae0..546d3725d8 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -24,18 +24,10 @@ module Rails opts.on("-p", "--port=port", Integer, "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } opts.on("-b", "--binding=IP", String, - "Binds Rails to the specified IP.", "Default: 0.0.0.0") { |v| options[:Host] = v } + "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v } opts.on("-c", "--config=file", String, "Uses a custom rackup configuration.") { |v| options[:config] = v } opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true } - opts.on("-u", "--debugger", "Enables the debugger.") do - if RUBY_VERSION < '2.0.0' - options[:debugger] = true - else - puts "=> Notice: debugger option is ignored since Ruby 2.0 and " \ - "it will be removed in future versions." - end - end opts.on("-e", "--environment=name", String, "Specifies the environment to run this server under (test/development/production).", "Default: development") { |v| options[:environment] = v } @@ -86,9 +78,6 @@ module Rails def middleware middlewares = [] - if RUBY_VERSION < '2.0.0' - middlewares << [Rails::Rack::Debugger] if options[:debugger] - end middlewares << [::Rack::ContentLength] # FIXME: add Rack::Lock in the case people are using webrick. @@ -102,17 +91,12 @@ module Rails Hash.new(middlewares) end - def log_path - "log/#{options[:environment]}.log" - end - def default_options super.merge({ Port: 3000, DoNotReverseLookup: true, environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, daemonize: false, - debugger: false, pid: File.expand_path("tmp/pids/server.pid"), config: File.expand_path("config.ru") }) @@ -126,15 +110,11 @@ module Rails puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" puts "=> Run `rails server -h` for more startup options" - if options[:Host].to_s.match(/0\.0\.0\.0/) - puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)" - end - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end def create_tmp_directories - %w(cache pids sessions sockets).each do |dir_to_make| + %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end end diff --git a/railties/lib/rails/commands/update.rb b/railties/lib/rails/commands/update.rb deleted file mode 100644 index 59fae5c337..0000000000 --- a/railties/lib/rails/commands/update.rb +++ /dev/null @@ -1,9 +0,0 @@ -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'generators')) - -if ARGV.size == 0 - Rails::Generators.help - exit -end - -name = ARGV.shift -Rails::Generators.invoke name, ARGV, behavior: :skip diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index f5d7dede66..76364cea8f 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -18,11 +18,11 @@ module Rails # This will put the <tt>Magical::Unicorns</tt> middleware on the end of the stack. # You can use +insert_before+ if you wish to add a middleware before another: # - # config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns + # config.middleware.insert_before Rack::Head, Magical::Unicorns # # There's also +insert_after+ which will insert a middleware after another: # - # config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns + # config.middleware.insert_after Rack::Head, Magical::Unicorns # # Middlewares can also be completely swapped out and replaced with others: # @@ -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,9 +65,10 @@ 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 diff --git a/railties/lib/rails/deprecation.rb b/railties/lib/rails/deprecation.rb deleted file mode 100644 index 89f54069e9..0000000000 --- a/railties/lib/rails/deprecation.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'active_support/deprecation/proxy_wrappers' - -module Rails - class DeprecatedConstant < ActiveSupport::Deprecation::DeprecatedConstantProxy - def self.deprecate(old, current) - # double assignment is used to avoid "assigned but unused variable" warning - constant = constant = new(old, current) - eval "::#{old} = constant" - end - - private - - def target - ::Kernel.eval @new_const.to_s - end - end - - DeprecatedConstant.deprecate('RAILS_CACHE', '::Rails.cache') -end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index dc3da1eb41..e1d5caf790 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -110,8 +110,8 @@ module Rails # # == Endpoint # - # An engine can be also a rack application. It can be useful if you have a rack application that - # you would like to wrap with +Engine+ and provide some of the +Engine+'s features. + # An engine can also be a rack application. It can be useful if you have a rack application that + # you would like to wrap with +Engine+ and provide with some of the +Engine+'s features. # # To do that, use the +endpoint+ method: # @@ -217,7 +217,7 @@ module Rails # <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, + # your controllers, you also need to namespace all your routes. With an isolated engine, # the namespace is applied by default, so you can ignore it in routes: # # MyEngine::Engine.routes.draw do @@ -296,7 +296,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 +312,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 +328,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. # @@ -351,7 +351,7 @@ module Rails base.called_from = begin call_stack = if Kernel.respond_to?(:caller_locations) - caller_locations.map(&:path) + caller_locations.map { |l| l.absolute_path || l.path } else # Remove the line number from backtraces making sure we don't leave anything behind caller.map { |p| p.sub(/:\d+.*/, '') } @@ -364,6 +364,10 @@ module Rails super end + def find_root(from) + find_root_with_flag "lib", from + end + def endpoint(endpoint = nil) @endpoint ||= nil @endpoint = endpoint if endpoint @@ -480,7 +484,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 @@ -531,7 +535,7 @@ module Rails # Define the configuration object for the engine. def config - @config ||= Engine::Configuration.new(find_root_with_flag("lib")) + @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from)) end # Load data from db/seeds.rb file. It can be used in to load engines' @@ -567,10 +571,10 @@ module Rails end initializer :add_routing_paths do |app| - paths = self.paths["config/routes.rb"].existent + routing_paths = self.paths["config/routes.rb"].existent - if routes? || paths.any? - app.routes_reloader.paths.unshift(*paths) + if routes? || routing_paths.any? + app.routes_reloader.paths.unshift(*routing_paths) app.routes_reloader.route_sets << routes end end @@ -595,12 +599,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) @@ -658,8 +656,7 @@ module Rails paths["db/migrate"].existent.any? end - def find_root_with_flag(flag, default=nil) #:nodoc: - root_path = self.class.called_from + def self.find_root_with_flag(flag, root_path, default=nil) #:nodoc: while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") parent = File.dirname(root_path) diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 10d1821709..62a4139d07 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -39,7 +39,7 @@ module Rails @paths ||= begin paths = Rails::Paths::Root.new(@root) - paths.add "app", eager_load: true, glob: "*" + paths.add "app", eager_load: true, glob: "{*,*/concerns}" paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true paths.add "app/helpers", eager_load: true @@ -47,9 +47,6 @@ module Rails paths.add "app/mailers", eager_load: true paths.add "app/views" - paths.add "app/controllers/concerns", eager_load: true - paths.add "app/models/concerns", eager_load: true - paths.add "lib", load_path: true paths.add "lib/assets", glob: "*" paths.add "lib/tasks", glob: "**/*.rake" diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 672b78c599..7d74b1bfe5 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -5,10 +5,10 @@ module Rails end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index bf2390cb7e..341291f08b 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -153,13 +153,13 @@ module Rails def self.invoke(namespace, args=ARGV, config={}) names = namespace.to_s.split(':') if klass = find_by_namespace(names.pop, names.any? && names.join(':')) - args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } + 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 @@ -226,7 +226,7 @@ module Rails def self.public_namespaces lookup! - subclasses.map { |k| k.namespace } + subclasses.map(&:namespace) end def self.print_generators @@ -260,11 +260,9 @@ 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 @@ -286,7 +284,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 b2c9d12996..c1bc646c65 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 @@ -85,10 +84,10 @@ module Rails # environment(nil, env: "development") do # "config.autoload_paths += %W(#{config.root}/extras)" # end - def environment(data=nil, options={}, &block) + def environment(data=nil, options={}) sentinel = /class [a-z_:]+ < Rails::Application/i env_file_sentinel = /Rails\.application\.configure do/ - data = block.call if !data && block_given? + data = yield if !data && block_given? in_root do if options[:env].nil? @@ -189,7 +188,7 @@ module Rails # generate(:authenticated, "user session") def generate(what, *args) log :generate, what - argument = args.flat_map {|arg| arg.to_s }.join(" ") + argument = args.flat_map(&:to_s).join(" ") in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) } end @@ -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}", { after: sentinel, verbose: false, force: true } end end diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index 682092fdf2..cffdef6ec9 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -39,7 +39,7 @@ module Rails protected - def on_conflict_behavior(&block) + def on_conflict_behavior options = base.options.merge(config) if identical? say_status :identical, :blue, relative_existing_migration @@ -48,7 +48,7 @@ module Rails say_status :create, :green unless pretend? ::FileUtils.rm_rf(existing_migration) - block.call + yield end elsif options[:skip] say_status :skip, :yellow diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 1cdad09391..253272c7dd 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -38,15 +38,13 @@ module Rails class_option :skip_keeps, type: :boolean, default: false, desc: 'Skip source control .keep files' + class_option :skip_action_mailer, type: :boolean, aliases: "-M", + default: false, + desc: "Skip Action Mailer files" + class_option :skip_active_record, type: :boolean, aliases: '-O', default: false, desc: 'Skip Active Record files' - class_option :skip_gems, type: :array, default: [], - desc: 'Skip the provided gems files' - - class_option :skip_action_view, type: :boolean, aliases: '-V', default: false, - desc: 'Skip Action View files' - class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false, desc: 'Skip Sprockets files' @@ -68,8 +66,11 @@ module Rails class_option :edge, type: :boolean, default: false, desc: "Setup the #{name} with Gemfile pointing to Rails repository" - class_option :skip_test_unit, type: :boolean, aliases: '-T', default: false, - desc: 'Skip Test::Unit files' + class_option :skip_turbolinks, type: :boolean, default: false, + desc: 'Skip turbolinks gem' + + 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" @@ -82,7 +83,7 @@ module Rails end def initialize(*args) - @gem_filter = lambda { |gem| !options[:skip_gems].include?(gem.name) } + @gem_filter = lambda { |gem| true } @extra_entries = [] super convert_database_option_for_jruby @@ -112,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 @@ -126,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 @@ -167,7 +167,7 @@ module Rails end def include_all_railties? - !options[:skip_active_record] && !options[:skip_action_view] && !options[:skip_test_unit] && !options[:skip_sprockets] + options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets).none? end def comment_if(value) @@ -183,8 +183,12 @@ module Rails super end - def self.github(name, github, comment = nil) - new(name, nil, comment, github: github) + def self.github(name, github, branch = nil, comment = nil) + if branch + new(name, nil, comment, github: github, branch: branch) + else + new(name, nil, comment, github: github) + end end def self.version(name, version, comment = nil) @@ -198,9 +202,15 @@ module Rails def rails_gemfile_entry if options.dev? - [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH)] + [ + GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), + GemfileEntry.github('arel', 'rails/arel') + ] elsif options.edge? - [GemfileEntry.github('rails', 'rails/rails')] + [ + GemfileEntry.github('rails', 'rails/rails'), + GemfileEntry.github('arel', 'rails/arel') + ] else [GemfileEntry.version('rails', Rails::VERSION::STRING, @@ -239,16 +249,8 @@ module Rails return [] if options[:skip_sprockets] gems = [] - if options.dev? || options.edge? - gems << GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails', - 'Use edge version of sprockets-rails') - gems << GemfileEntry.github('sass-rails', 'rails/sass-rails', - 'Use SCSS for stylesheets') - else - gems << GemfileEntry.version('sass-rails', - '~> 5.0.0.beta1', + gems << GemfileEntry.version('sass-rails', '~> 5.0', 'Use SCSS for stylesheets') - end gems << GemfileEntry.version('uglifier', '>= 1.3.0', @@ -262,17 +264,12 @@ module Rails 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 .js.coffee assets and views' + comment = 'Use CoffeeScript for .coffee assets and views' if options.dev? || options.edge? - GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', comment + GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, comment else - GemfileEntry.version 'coffee-rails', '~> 4.0.0', comment + GemfileEntry.version 'coffee-rails', '~> 4.1.0', comment end end @@ -281,17 +278,14 @@ module Rails [] else gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry] + gems << GemfileEntry.version("#{options[:javascript]}-rails", nil, + "Use #{options[:javascript]} as the JavaScript library") - if options[:javascript] == 'jquery' - gems << GemfileEntry.version('jquery-rails', '~> 4.0.0.beta2', - 'Use jQuery as the JavaScript library') - else - gems << GemfileEntry.version("#{options[:javascript]}-rails", nil, - "Use #{options[:javascript]} as the JavaScript library") + unless options[:skip_turbolinks] + gems << GemfileEntry.version("turbolinks", nil, + "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks") end - gems << GemfileEntry.version("turbolinks", nil, - "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks") gems end end @@ -342,7 +336,7 @@ module Rails end def spring_install? - !options[:skip_spring] && Process.respond_to?(:fork) + !options[:skip_spring] && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end def run_bundle diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 9af6435f23..813b8b629e 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -273,7 +273,7 @@ module Rails # Use Rails default banner. def self.banner - "rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]".gsub(/\s+/, ' ') + "rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, ' ') end # Sets the base_name taking into account the current class namespace. diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 66b17bd10e..65563aa6db 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -1,13 +1,40 @@ -require 'rails/generators/erb/controller/controller_generator' +require 'rails/generators/erb' module Erb # :nodoc: module Generators # :nodoc: - class MailerGenerator < ControllerGenerator # :nodoc: + class MailerGenerator < Base # :nodoc: + argument :actions, type: :array, default: [], banner: "method method" + + def copy_view_files + view_base_path = File.join("app/views", class_path, file_name + '_mailer') + empty_directory view_base_path + + if self.behavior == :invoke + formats.each do |format| + layout_path = File.join("app/views/layouts", filename_with_extensions("mailer", format)) + template filename_with_extensions(:layout, format), layout_path + end + end + + actions.each do |action| + @action = action + + formats.each do |format| + @path = File.join(view_base_path, filename_with_extensions(action, format)) + template filename_with_extensions(:view, format), @path + end + end + end + protected 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/mailer/templates/layout.html.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb new file mode 100644 index 0000000000..93110e74ad --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb @@ -0,0 +1,5 @@ +<html> + <body> + <%%= yield %> + </body> +</html> diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb new file mode 100644 index 0000000000..6363733e6e --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb @@ -0,0 +1 @@ +<%%= yield %> 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 bba9141fb8..d9713b0238 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -1,10 +1,10 @@ -<%%= form_for(@<%= singular_table_name %>) do |f| %> - <%% if @<%= singular_table_name %>.errors.any? %> +<%%= form_for(<%= singular_table_name %>) do |f| %> + <%% if <%= singular_table_name %>.errors.any? %> <div id="error_explanation"> - <h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> + <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> <ul> - <%% @<%= singular_table_name %>.errors.full_messages.each do |message| %> + <%% <%= singular_table_name %>.errors.full_messages.each do |message| %> <li><%%= message %></li> <%% end %> </ul> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb index 5620fcc850..81329473d9 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb @@ -1,6 +1,6 @@ <h1>Editing <%= singular_table_name.titleize %></h1> -<%%= render 'form' %> +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> <%%= link_to 'Show', @<%= singular_table_name %> %> | <%%= link_to 'Back', <%= index_helper %>_path %> 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/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb index db13a5d870..9b2b2f4875 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb @@ -1,5 +1,5 @@ <h1>New <%= singular_table_name.titleize %></h1> -<%%= render 'form' %> +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> <%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index f16bd8e082..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 @@ -159,6 +163,10 @@ module Rails options.delete(:required) options[:null] = false end + + if reference? && !polymorphic? + options[:foreign_key] = true + end end end end 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 b7da44ca2d..36456e64f5 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -99,7 +99,7 @@ module Rails end def class_name - (class_path + [file_name]).map!{ |m| m.camelize }.join('::') + (class_path + [file_name]).map!(&:camelize).join('::') end def human_name @@ -145,7 +145,7 @@ module Rails @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name end - # Tries to retrieve the application name or simple return application. + # Tries to retrieve the application name or simply return application. def application_name if defined?(Rails) && Rails.application Rails.application.class.name.split('::').first.underscore @@ -156,7 +156,7 @@ module Rails def assign_names!(name) #:nodoc: @class_path = name.include?('/') ? name.split('/') : name.split('::') - @class_path.map! { |m| m.underscore } + @class_path.map!(&:underscore) @file_name = @class_path.pop end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 9110c129d1..899b33e529 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 @@ -88,12 +88,22 @@ 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 + unless callback_terminator_config_exist + remove_file 'config/initializers/callback_terminator.rb' + end + 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 @@ -120,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' @@ -229,7 +240,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 @@ -252,6 +263,12 @@ 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 finish_template build(:leftovers) end @@ -260,9 +277,7 @@ module Rails public_task :generate_spring_binstubs def run_after_bundle_callbacks - @after_bundle_callbacks.each do |callback| - callback.call - end + @after_bundle_callbacks.each(&:call) end protected @@ -340,7 +355,7 @@ module Rails # # This class should be called before the AppGenerator is required and started # since it configures and mutates ARGV correctly. - class ARGVScrubber # :nodoc + class ARGVScrubber # :nodoc: def initialize(argv = ARGV) @argv = argv end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 7b1a6aa854..82a0315379 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -15,9 +15,6 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' -# Use Rails Html Sanitizer for HTML sanitization -gem 'rails-html-sanitizer', '~> 1.0' - # Use Unicorn as the app server # gem 'unicorn' @@ -25,24 +22,28 @@ gem 'rails-html-sanitizer', '~> 1.0' # gem 'capistrano-rails', group: :development group :development, :test do -<% unless defined?(JRUBY_VERSION) -%> - # Call 'debugger' anywhere in the code to stop execution and get a debugger console - <%- if RUBY_VERSION < '2.0.0' -%> - gem 'debugger' - <%- else -%> +<% if RUBY_ENGINE == 'ruby' -%> + # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' - <%- end -%> # Access an IRB console on exception pages or by using <%%= console %> in views - gem 'web-console', github: 'rails/web-console', branch: 'master' + <%- if options.dev? || options.edge? -%> + gem 'web-console', github: "rails/web-console" + <%- else -%> + gem 'web-console', '~> 2.0' + <%- end -%> <%- if spring_install? %> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' <% end -%> + + # Adds `Method#source` and `Method#comment` to get the source code of a + # method from the console + gem 'method_source' <% end -%> end +<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> -<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince/) -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] +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 07ea09cdbd..c1a77944e8 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 @@ -2,7 +2,7 @@ // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. 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 a443db3401..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 @@ -3,12 +3,11 @@ * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * 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/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails index 6a128b95e5..80ec8080ab 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/rails +++ b/railties/lib/rails/generators/rails/app/templates/bin/rails @@ -1,3 +1,3 @@ -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup index 0e22b3fa5c..eee810be30 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -1,28 +1,30 @@ require 'pathname' +require 'fileutils' +include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) -Dir.chdir APP_ROOT do +chdir APP_ROOT do # This script is a starting point to setup your application. - # Add necessary setup steps to this file: + # Add necessary setup steps to this file. - puts "== Installing dependencies ==" - system "gem install bundler --conservative" - system "bundle check || bundle install" + puts '== Installing dependencies ==' + system 'gem install bundler --conservative' + system('bundle check') or system('bundle install') # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system 'ruby bin/rake db:setup' puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + rm_f Dir.glob('log/*') + rm_rf 'tmp/cache' puts "\n== Restarting application server ==" - system "touch tmp/restart.txt" + touch 'tmp/restart.txt' end diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru index 5bc2a619e8..bd83b25412 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru @@ -1,4 +1,4 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __FILE__) run Rails.application 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 9445c2454e..a2661bfb51 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -3,15 +3,16 @@ 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" <%= comment_if :skip_active_record %>require "active_record/railtie" require "action_controller/railtie" -require "action_mailer/railtie" -<%= comment_if :skip_action_view %>require "action_view/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,10 +32,5 @@ 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 - <%- unless options.skip_active_record? -%> - - # Do not swallow errors in after_commit/after_rollback callbacks. - config.active_record.raise_in_transactional_callbacks = true - <%- end -%> end end 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 4b2e6646c7..596c916573 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -1,6 +1,6 @@ # MySQL. Versions 5.0+ are recommended. # -# Install the MYSQL driver +# Install the MySQL driver # gem install mysql2 # # Ensure the MySQL gem is defined in your Gemfile diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index d8326d1728..ecb5d4170f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -12,9 +12,11 @@ Rails.application.configure do # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + <%- end -%> # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log 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 2cd8040f35..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,13 +14,9 @@ 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 Rails's static asset server (Apache or NGINX will already do this). - config.serve_static_assets = false + # 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? <%- unless options.skip_sprockets? -%> # Compress JavaScripts and CSS. @@ -37,18 +33,22 @@ 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-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true - # Decrease the log volume. - # config.log_level = :info + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + 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) @@ -56,12 +56,15 @@ 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. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + <%- end -%> # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index 32756eb88b..0306deb18c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -12,8 +12,8 @@ Rails.application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_assets = true + # Configure static file server for tests with Cache-Control for performance. + config.serve_static_files = true config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. @@ -25,13 +25,15 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + <%- unless options.skip_action_mailer? -%> # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + <%- end -%> - # Randomize the order test cases are executed + # Randomize the order test cases are executed. config.active_support.test_order = :random # Print deprecation notices to the stderr. 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 new file mode 100644 index 0000000000..e63022da91 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb @@ -0,0 +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 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..94f612c3dd 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 @@ -11,6 +11,6 @@ end # 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 8775e5e235..7c6f2098b8 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -14,5 +14,6 @@ <% end -%> # Ignore all logfiles and tempfiles. -/log/*.log +/log/* +!/log/.keep /tmp diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index a48cc13ed7..df615c88b5 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -12,7 +12,7 @@ module Rails def add_routes unless options[:skip_routes] - actions.reverse.each do |action| + actions.reverse_each do |action| route generate_routing_code(action) end 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 584f776c01..68c3829515 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -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} @@ -74,7 +74,8 @@ task default: :test end PASSTHROUGH_OPTIONS = [ - :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip + :skip_active_record, :skip_action_mailer, :skip_javascript, :database, + :javascript, :quiet, :pretend, :force, :skip ] def generate_test_dummy(force = false) @@ -116,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 @@ -127,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 @@ -225,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 @@ -255,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 @@ -293,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 @@ -304,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 @@ -327,8 +357,10 @@ 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) 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 35ad9fbf9e..2c91c6a0ea 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -37,11 +37,11 @@ end <% end -%> <% end -%> -<% unless defined?(JRUBY_VERSION) -%> +<% if RUBY_ENGINE == 'ruby' -%> # To use a debugger - <%- if RUBY_VERSION < '2.0.0' -%> -# gem 'debugger', group: [:development, :test] - <%- else -%> # gem 'byebug', group: [:development, :test] - <%- end -%> +<% 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 5508829f6b..b1038c839e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb @@ -6,13 +6,13 @@ require 'rails/all' # Pick the frameworks you want: <%= comment_if :skip_active_record %>require "active_record/railtie" require "action_controller/railtie" -require "action_mailer/railtie" -<%= comment_if :skip_action_view %>require "action_view/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 5bc2e1c8b5..c28e5badc6 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js @@ -2,7 +2,7 @@ // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. 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 a443db3401..0cdd2788d0 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css @@ -3,12 +3,11 @@ * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * 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 1e26a313cd..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 @@ -1,15 +1,25 @@ # Configure Rails Environment ENV["RAILS_ENV"] = "test" -require File.expand_path("../dummy/config/environment.rb", __FILE__) +require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__) +<% unless options[:skip_active_record] -%> +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)] +<% if options[:mountable] -%> +ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) +<% end -%> +<% end -%> require "rails/test_help" -Rails.backtrace_cleaner.remove_silencers! +# Filter out Minitest backtrace while allowing backtrace from other libraries +# to be shown. +Minitest.backtrace_filter = Minitest::BacktraceFilter.new # Load support files Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } # Load fixtures from the engine -if ActiveSupport::TestCase.method_defined?(:fixture_path=) +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/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 1ae7000299..69af1e8307 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -4,6 +4,7 @@ body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; + margin: 33px; } pre { @@ -16,6 +17,16 @@ 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 { margin-bottom: 10px; } 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 6bf0a33a5f..c01b82884d 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 @@ -7,6 +7,7 @@ module Rails check_class_collision suffix: "Controller" + class_option :helper, type: :boolean class_option :orm, banner: "NAME", type: :string, required: true, desc: "ORM to generate the controller for" diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 4669935156..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 @@ -39,7 +39,7 @@ module Rails def assign_controller_names!(name) @controller_name = name @controller_class_path = name.include?('/') ? name.split('/') : name.split('::') - @controller_class_path.map! { |m| m.underscore } + @controller_class_path.map!(&:underscore) @controller_file_name = @controller_class_path.pop end @@ -48,7 +48,7 @@ module Rails end def controller_class_name - (controller_class_path + [controller_file_name]).map!{ |m| m.camelize }.join('::') + (controller_class_path + [controller_file_name]).map!(&:camelize).join('::') end def controller_i18n_scope diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb index 05d9fde280..566b61ca66 100644 --- a/railties/lib/rails/generators/test_unit/job/job_generator.rb +++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb @@ -6,7 +6,7 @@ module TestUnit # :nodoc: check_class_collision suffix: 'JobTest' def create_test_file - template 'unit_test.rb.erb', File.join('test/job', class_path, "#{file_name}_test.rb") + template 'unit_test.rb.erb', File.join('test/jobs', class_path, "#{file_name}_job_test.rb") end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 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..6b85764a66 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 +class <%= class_name %>MailerPreview < ActionMailer::Preview <% actions.each do |action| -%> # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> def <%= action %> - <%= class_name %>.<%= action %> + <%= class_name %>Mailer.<%= action %> end <% end -%> diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index f19e9d1d87..50ca61a35b 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -5,6 +5,8 @@ <% attributes.each do |attribute| -%> <%- if attribute.password_digest? -%> password_digest: <%%= BCrypt::Password.create('secret') %> + <%- elsif attribute.reference? -%> + <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default) %> <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- 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..8d825ae7b0 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 @@ -19,30 +19,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 %>)) 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} }" %> + patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= 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 e0600d0b59..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 @@ -50,7 +52,7 @@ module Rails # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator # destination File.expand_path("../tmp", File.dirname(__FILE__)) - # teardown :cleanup_destination_root + # setup :prepare_destination # # test "database.yml is not created when skipping Active Record" do # run_generator %w(myapp --skip-active-record) @@ -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.rb b/railties/lib/rails/info.rb index 9502876ebb..5909446b66 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,11 +1,14 @@ require "cgi" module Rails + # This module helps build the runtime properties used to display in the + # Rails::InfoController responses. Including the active Rails version, Ruby + # version, Rack version, and so on. module Info mattr_accessor :properties class << (@@properties = []) def names - map {|val| val.first } + map(&:first) end def value_for(property_name) @@ -22,19 +25,8 @@ module Rails rescue Exception end - def frameworks - %w( active_record action_pack action_view action_mailer active_support active_model ) - end - - def framework_version(framework) - if Object.const_defined?(framework.classify) - require "#{framework}/version" - framework.classify.constantize.version.to_s - end - end - def to_s - column_width = properties.names.map {|name| name.length}.max + column_width = properties.names.map(&:length).max info = properties.map do |name, value| value = value.join(", ") if value.is_a?(Array) "%-#{column_width}s %s" % [name, value] @@ -61,6 +53,11 @@ module Rails end end + # The Rails version. + property 'Rails version' do + Rails.version.to_s + end + # The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)". property 'Ruby version' do "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})" @@ -75,23 +72,10 @@ module Rails ::Rack.release end - # The Rails version. - property 'Rails version' do - Rails.version.to_s - end - property 'JavaScript Runtime' do ExecJS.runtime.name end - # Versions of each Rails framework (Active Record, Action Pack, - # Action Mailer, and Active Support). - frameworks.each do |framework| - property "#{framework.titlecase} version" do - framework_version(framework) - end - end - property 'Middleware' do Rails.configuration.middleware.map(&:inspect) end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 3eb66c07af..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"] @@ -77,23 +77,23 @@ module Rails end def all_paths - values.tap { |v| v.uniq! } + values.tap(&:uniq!) end def autoload_once - filter_by { |p| p.autoload_once? } + filter_by(&:autoload_once?) end def eager_load - filter_by { |p| p.eager_load? } + filter_by(&:eager_load?) end def autoload_paths - filter_by { |p| p.autoload? } + filter_by(&:autoload?) end def load_paths - filter_by { |p| p.load_path? } + filter_by(&:load_path?) end private @@ -167,8 +167,8 @@ module Rails @paths.concat paths end - def unshift(path) - @paths.unshift path + def unshift(*paths) + @paths.unshift(*paths) end def to_ary diff --git a/railties/lib/rails/rack.rb b/railties/lib/rails/rack.rb index 886f0e52e1..a4c4527a72 100644 --- a/railties/lib/rails/rack.rb +++ b/railties/lib/rails/rack.rb @@ -1,7 +1,5 @@ module Rails module Rack - autoload :Debugger, "rails/rack/debugger" if RUBY_VERSION < '2.0.0' - autoload :Logger, "rails/rack/logger" - autoload :LogTailer, "rails/rack/log_tailer" + autoload :Logger, "rails/rack/logger" end end diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb index f7b77bcb3b..1fde3db070 100644 --- a/railties/lib/rails/rack/debugger.rb +++ b/railties/lib/rails/rack/debugger.rb @@ -1,24 +1,3 @@ -module Rails - module Rack - class Debugger - def initialize(app) - @app = app +require 'active_support/deprecation' - ARGV.clear # clear ARGV so that rails server options aren't passed to IRB - - require 'debugger' - - ::Debugger.start - ::Debugger.settings[:autoeval] = true if ::Debugger.respond_to?(:settings) - puts "=> Debugger enabled" - rescue LoadError - puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again." - exit(1) - end - - def call(env) - @app.call(env) - end - end - end -end +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb deleted file mode 100644 index bc26421a9e..0000000000 --- a/railties/lib/rails/rack/log_tailer.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'active_support/deprecation' - -module Rails - module Rack - class LogTailer - def initialize(app, log = nil) - ActiveSupport::Deprecation.warn "LogTailer is deprecated and will be removed on Rails 5" - - @app = app - - path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath - - @cursor = @file = nil - if ::File.exist?(path) - @cursor = ::File.size(path) - @file = ::File.open(path, 'r') - end - end - - def call(env) - response = @app.call(env) - tail! - response - end - - def tail! - return unless @cursor - @file.seek @cursor - - unless @file.eof? - contents = @file.read - @cursor = @file.tell - $stdout.print contents - end - end - end - end -end 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 df74643a59..aea3d2339c 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,13 +1,13 @@ -if RUBY_VERSION < '1.9.3' +if RUBY_VERSION < '2.2.0' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 4 prefers to run on Ruby 2.1 or newer. + Rails 5 requires to run on Ruby 2.2.0 or newer. You're running #{desc} - Please upgrade to Ruby 1.9.3 or newer to continue. + Please upgrade to Ruby 2.2.0 or newer to continue. end_message end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 201532d299..9b058a1848 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -80,9 +80,8 @@ class SourceAnnotationExtractor # Returns a hash that maps filenames under +dir+ (recursively) to arrays # with their annotations. Only files with annotations are included. Files - # with extension +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+, - # +.scss+, +.js+, +.coffee+, +.rake+, +.sass+ and +.less+ - # are taken into account. + # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+, + # +.css+, +.js+ and +.erb+ are taken into account. def find_in(dir) results = {} diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index af5f2707b1..945fbdb3e2 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -1,7 +1,8 @@ +require 'rake' + # Load Rails Rakefile extensions %w( annotations - documentation framework log middleware 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/statistics.rake b/railties/lib/rails/tasks/statistics.rake index b94cd244be..735c36eb3a 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -1,6 +1,6 @@ -# while having global constant is not good, -# many 3rd party tools depend on it, like rspec-rails, cucumber-rails, etc -# so if will be removed - deprecation warning is needed +# While global constants are bad, many 3rd party tools depend on this one (e.g +# rspec-rails & cucumber-rails). So a deprecation warning is needed if we want +# to remove it. STATS_DIRECTORIES = [ %w(Controllers app/controllers), %w(Helpers app/helpers), @@ -14,10 +14,9 @@ STATS_DIRECTORIES = [ %w(Helper\ tests test/helpers), %w(Model\ tests test/models), %w(Mailer\ tests test/mailers), + %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| +].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 116988665f..9162ef234a 100644 --- a/railties/lib/rails/tasks/tmp.rake +++ b/railties/lib/rails/tasks/tmp.rake @@ -1,9 +1,8 @@ namespace :tmp do - desc "Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear)" - task clear: [ "tmp:sessions:clear", "tmp:cache:clear", "tmp:sockets:clear"] + desc "Clear cache and socket files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear)" + task clear: ["tmp:cache:clear", "tmp:sockets:clear"] - tmp_dirs = [ 'tmp/sessions', - 'tmp/cache', + tmp_dirs = [ 'tmp/cache', 'tmp/sockets', 'tmp/pids', 'tmp/cache/assets/development', @@ -12,16 +11,9 @@ 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 :sessions do - # desc "Clears all files in tmp/sessions" - task :clear do - FileUtils.rm(Dir['tmp/sessions/[^.]*']) - end - end - namespace :cache do # desc "Clears all files and directories in tmp/cache" task :clear do diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 1dc1d70f8d..0b08a01896 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -2,6 +2,10 @@ <html><head> <meta name="viewport" content="width=device-width" /> <style type="text/css"> + html, body, iframe { + height: 100%; + } + body { margin: 0; } @@ -38,7 +42,6 @@ iframe { border: 0; width: 100%; - height: 800px; } </style> </head> diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index eb620caa00..6726c23fc9 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -19,13 +19,13 @@ } a {color: #03c} + a:hover { background-color: #03c; color: white; text-decoration: none; } - #page { background-color: #f0f0f0; width: 750px; @@ -57,21 +57,21 @@ padding-right: 30px; } - #header { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAABACAYAAABY1SR7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAGZhJREFUeNqsWwmUXGWV/t5Sr9aurl6qO0l3Z9/DEoJh18gZQGAUxPHIyQHH7eioZ8bjnAFHZ0RndNxxRBhGcUbxoKIHBkTEcUYREIHIGpKQjUDS6U660/tSVV3Lq/fefPf/Xy2dBFGYx3npqvde/e/e/97v3u/e/8e4Lt2L8DCCAFcGwF8ZBjYbgM1rAZoO+WLwZhDMu9y4+YcOozbAqzwXNA3GdzX/5hV+KnKO2+GXFj/AvzmW8e72iG202CYiphbY403f9/k3QHZtJ9oWtyCQe7wGX79TKVb7rP9pXJPDVxf0Rz+oyxm4HNWrahFNixdk3EAJbERMWOm4ulctVODNVeEVK0DeRVDgb1wfJgcqUo6duaKnFOH7bm6JmH+5LOEgZprwRIHAV3JYfLjKM55Noz3bBqdcgt0Wg52Kq/cHHkXns0qIukKBlltk9rU2QaiouiefPQ+RdBuseAJeqYTK1CTH8mE4NsyIpRWu8nssCs+xULWpjGVwTvieKl/sV6mIXzOib/OftzuG8d6l8SiVMODyRb46oazg8YPP2Wnvy9ISNqplzsxYAW6hjGhHEmYiBoPC+hRMfFMrESgrBC5n0KS+lq1nPahZh2OXymg9bSNWX/u3FKyKI//7Exx96B4Y8RiCEseq8t0VznyxjMDidFIJ8QSf3hJEOFbZEAHVhIkFTX54fxtnIW5pJUQIeZ8ooZShkInuDOLpFIX1ldtCBix7KI/k4E7OwbTjcNIdiCQzsONp2LEk7GgUnZsuQN9lW2En45xlukrUghWzeZq8FsXsi8+gND6MSCqD9k3nwulIUShKZxt0LYPWortRSY0NXreC8J6pZNDChEDh53PT1NIPLaEnLbQKNTETEaR7sycA0jD1INXZAnzObjTbiWh7Vr1A3Knn4nciu+lCvstUig09cp96cVCtcELoFpEIFUjjyIM/osWIg+IMXS3DcfNwZ3NQHmmKU9OqroX2jWdgatduuPkpmA4ViZrK9RqKABEBtg9tDeW+oUIyTIYuFaX7eCG4aqbU+hhKocD3UBoZISBLiC9cpAQKyq5SQo6OjVswtec5VHLTiHUuIN4WonXlqUj2riS0DIUXwZlERFHSK+SQGzqI3MHdmNm7CzMvvowF527B8qvejZ3/+iXk9vVTao5tiTKN0OUHISZEGS/8W6UbRdoTSHe3E1f+CRaR3xhBLVJSIQ7qleZQGBigZYoYdR+ElUjBaW3H6JMPIrV0Hdo2bEayZ7my0KsdLctPBS64EuWZMYw/9wTGnvod0mtzWH71Vuz66o10bVpK8FIx6orUMejpCKYBTvfM9HXBJtA8z3/1BKDivaksVJmaYsgsYPDnd6LzzAuw8I1XUIGleC1HtDWLnguv5BiX4+jDD2D4sQeV1bQvNXBi6vAb1MGtrEEHjRPgqfZ0qMRJElYYSudfq12nmzAvtJ2yib69iRadRGnySD0Uv5bDtCPou/gqnPY3N6DnLRczgtHxCf4aVnUeUdgw6i6FqM1w292Ujo/TJdB5wHcJ2iDCaBTRmVfw4rkw4yksuvQyJJf0YvrgNiayvBLESS9AYuFqJLLLCPb4SQWulosojhxmeCeoDeaQSoVuy8lPtSKxYKnC2Bmf+DwtvBgv3/qfTI6uEtGuJV7PCBTIq5zNtt5uxBgyvap30pf55TISfX1Y/PatGPrVvcgPvEyAJ1GenaPZLSy//G2IL+qki43CNCMwk620iovy9FGUJgYwm8gwpK/guRJOS5dyD688h+n9z2L28F4Ujx2ia04jEl8Ad3oGVTePaGcnQ3sKLb1rkD3nIqx594dRIh733n6PmmrrvGj671sjVlxczRWAkxZ0r+rTrhfMJ0uEM8xKUYXONR+5nr57BdpP24TCsX6M/f5F5AYLWPauK9F11htUwjOIL8GNZH1qpKwiyVGELk0OoDj2EtziFOaODSN3aC/v24xmZzAU51TgcJKd/DktHo9jyRXvg0Or7PvejTj22KPKiyafew6zg8MYypVLNsLkJ2bxaZXM4i5EmCBPsEaoWJUUpfeSK7DgvEtQmh4ihTDQdf5FOHDHr7HqPVeh99KL4OVzpE50N18CtqnCdBCY6rsEcTsqIGUGD6rY9e3bMPzIHmTWLsbqa7ai84wL6YrTqEyOqEmwonEExSoO//R7dLcJWiWCueF+7P7mjZAUY8YdJZqySMo24j5zQSybQdeyhdrX5imho4NhEEnkRbkDQyjSRVJLeziCgef/6avIrFuOtR95P2lJNSSshg4l6rdm+Ht9inWsqIOX7voN+u/eRoEM5PvHMbbjGcwcfg7jO3YxbCcRiaaYQOXnpEaFGeahGQaMCidJRidt8RghS6Q344XQIowmFq2QXdLNdwsx8zUFqCOQNIECVqdp8pESB53Fvhdux9T2FxBb1AWX4XbjDX/HFzjEmgedB4XYKT5D4T0VTLRCtIiTwOBvfovpvS8T+Bm4MyW6jw13tIIDt/9G/TTWk8HKvzgbmd4+YldYQIdixgHJYkC82Ul6UDnQSbEGdsFGZlEWyUyLyiEyYwajRVAoAXNlEjR+pjUCUmiDQcKOORwwgpFfP4cg5mPzTZ9FoqePdGVWuZRPYQNPcgrd0/dCpqpdy3DIsQ4fxtiTu7Hxkx8iRXkcB+94iM86/K0Jx4opi5aOzGJs14toWeLAdYXWxFQCtJlkA+LUq+bI7QR3mj3YoqVNgGcXd5NWUOiZAk9GH86S4jK25jWBLVREl1uK5Voywz6WXf1WLHjTm0lPigSyxoUpnEqU8c26Wyk/Y24RMjhw/yMoj+cQbWvH0isuwuijL6BwaJwcyq7XUTaBP7N3HOU3ke7HSONJb8RTBGoGKZPFyTE8saTZyCPtrC2coxOoTuY5+x4UTzHNsNjR6d6Qa8JJ5BIV8ksVtKzpwcr3v5dyOrzHKMWXizsZAnK6k1ImPDmAqjOmdr9AwXcodzr4kwfQfuY6VKbzyhpGU96S75WxIqb2DaPnvNWKklQD4WSuzB+sVILjOYjm/VARSWKTBQQzlZCFmErYeubzVJJR14SlQtVQMjO0xrXvoulXkq3OKnxAXqSsoSmNUbOM/BV35RjDDz9JrBXpnnEM3vsYjj38LLyZihI8QNAgQhITOCmTO46i+6w1MPm86RVIiC09/RJUGcECCe2UU0G6QIyUjEC5hGaCNd4RqHKU6VuDylQlI2N8hfXDWibEdyhCKXREuZUVUX8lyhh2+Jl5Q/6akSgT4izGn3wBFu+JwYOKj8qwtsbJaYmJuYEZ5AYmFOWXPCN1jTodzeuqM0WtSI1rzXrV0LSNKRFuZLYQ2EYVPjEQVuQUMsCya65GvL1HWUwJS+FNUcBsUiZUQv7aLGlndr+I8ug4XUMVAJw4U7FmI8SFETTmUaGK2gas1SeeP8znoizIEso9DaUIy2FWkNU5V0VYs/azWXKncuCHqgQq1CHiY831H8TGr34erRvXKdD6LD3b+HnRn12qGgdqlmxHZe2aRcy6NbQScl8y8dSOfWQE1yK9YYmqXYww3xhNObemUI2IWraF2d1HMTeeh83MbkUiylKiiMdy2wjzXBjxWYdRiSkhfDVVKGSstxM9l16JxZe/E2+848c49bPXK9D2vPUyEsBOVZMINmpCW6HgEOuIQjXF6FYuAV2aHsWyrVfj9C9er5SR5Kms0PTf8QoZtIo7WSJW+mmRJLGSpDK2ipzV2bK6X6fxtWOCicYVqyhGXkXn+WeTcfape5ZDsPGM91C5iy8LI0s445bd9FkrAFHICt1N8DE+gdyeQczs34+uzeei68LNLGfdea50st6VbiyYmHq+nxTFRSSRVsD3ii7xyeQbdt/M5h/MERMT4i6GjlAWeUxh6HCN8+LIz+5H5zlUbtHSOnVp4MCa51JaIQ16i0kwP9CP0uExPP+JL2DggfuYN8jTJClYxnH4aNimdpp0r7nDkyx9h5gE0+RqSVTyZXXTsMz5FaJyMJrrGLNopyWUIImj//1LjPzuUZLCC5gzVqMwPIglV7/rxCaihFaCPCDOxDUl1EoylFP4mUlFCgPDStLKWB47PnUjrSSsNqrJsa/zR02ZwGjYRoVkEZh0ZHzbfmTPXE85SWrnKip6GeFE2I1iKVBCzNK9pmiVhS1x+Axx7myRJesvgHvvR3rNKmQ3n/OKPVGND1MVXTqHiFK6qVFiwlXgTVDhkq+ChhnyJCW9GeaoIGQOdV0M9YhYZWbvUXrIJJ+rKL6lJ9CYj5Fai0iKqyPkx0HcUsJYrBbtREIJ2H72GxTI/2CL1zAbLkZ8WIxYgUvsKebq6Zl3rEZvymx6echo1N+au9XcS3oHsxWMPrGTFH+CLhsmbhMNRWrNB4SZVSwyJ5WDFRb3DAAmaXf2rPP+6BpbkmStkBLAWwkHmdNWKfYqFaZRp2GGdo+mhpv6bBkNhepRzERpdASeW1aKSZ5RidpoUsRAvQ+NJCnJHHl+bcZ80vjkij661vo/rWMQSitWskgnNv7LP+MNN38NadYuCPtYCItIFTjMRgfeqClkhkFZ+FXCQmpFuyKXii7xNI93LT9szdrUMsNZnJkuwZX6zlKdaqRXrESiq/e19kBC3NisLt+Gc/7jW0gtZ51Bl1MCmUaoM//aRv0aapnF0l362KIUnI6EyuhCUOuWrIVfAZcRAj5NJWJ0C5epP19y1awJLWhdt/a1t3KcGF8Yxb5bbsLItoeYmxZRkRWq46IrR9StX/tcw4oKsYH+nlrZpmbcZQ7R1tDPBvMbdIwofLpVKIfcJy5nCa5WRhnDFkVOx+s5kr29GPzpfUxsuxg0zlQUxSZudG/CqNOSIJxYCclGCA7fDRDpiCK6gIVfidVmWXrHRh0fmBd+eSYIIEcWdRhdJJsWp+aQT1vI9nYjnl3wuhSJLuhAJJ1WQWDisadUELCi0bD1WlscMpq6lrV1Ft0riC9tVcFD8odfDVS9bod5pNGgC3+XFnxsXA2rsw25/gHMTcwiRxdbvLgPsY7s61IktWSZinw6l8SbupNGvUlphB1yZY3aIhfZtRmz4XS3oMoA5JP6BywdvBIr24ytMdzsWjHaMcnI0nXRG5FkdCrnS6gy6QzccxeMZDsJW+r1KbJ4pbKAVy6huXoyauVUaAUjRK5WjN9cH05PCiZl84VfsXaSVTKf191C6F61qCXjtjAORtvTSPb0sgYoEi/UmEmnMj6JkpXA6z2cTAbxxV26GdEEZB12DVVV63BrIuwYaWpCGZyuJBWSFSxPLTB5PH1+rhDDKlQbuvajNUzE+UVyRTTdQt+zWIrGWIJOozo8hjmashq8PkXsZAoty1Yqi/gVnq6ru+p1pUKFTM3dENJzu421TiqKKq3hhUp45apSyM1VGMH0xOi+liz0yOxUyijs2w2DlRjI+8tHB3XUIP+fGBxA9+LFr1kRgwV769p1fPkEQ+9KRq+dKE9MsGKc1BmxltEC7W6CEdW0aUtocIvw0tcSt5JGu3R4OA+zIxW1uKoUOUZzFxmxRp/ai+iz+xi9CK5EVJGdqBNBlG4xdvBlRq9eTQteawhm0MgPLsSGj92gVqjKk8ew/TOfxPjjz8BKxhvLFGHjWUBuJh0Cu6pqD7WCTGz4BDqKpE30rIlj05rw6sKFxuCXPP9O8MEjxQqOTuQwNjJLa1mItaRRGB3GLHnO6znaNmxC/nA/cocPKNoS61iEZVdfEy5LBHVKUieCLY5eeKIiXp6RapJuNVJFMCamYGnOUFyslBo0Xronai0dIfXmnZIqtKhgNIaj/F3ULSLx4j60dnXXy8s/OZe0dyGW7cLOL34arevXI9rayWgYhZPtoJtNqsTbyPKUgwzamyCw867MtG5NBUF9bSBXLCkeKOzDroUutaZODax52yUk5sfgsyrL897+PXtQHTmK7vWnomPpCkSTf3pI7j7/Qmz/5HWY3r5LNziYeC3WPlYsovOJJ7VKVbuPENcgXEyvuV3IbKXpPlcqqh0acqGe2S1oq1jzqmZ+b0mGDJNaM2bnjrHuPnYUifZOtDMKda9ah1RnZ30F99WO9jM2MzouZw0vLdJIuCsiUInOz0vbiVNa9DSBtITyWo3VAV/XG/KmPEuBKrmard7rNxKiyCoN7EBnpXlLCiYTmfibuEHSSSkLV4uzGNr5NEYP7EZb31J0rd6AzMIevtf+g4oIg+7e8iYM3H03J5muw9n3ZquqfwU3aGDdMBqdztr+lXBbhyg+R2xYTb5jN7YG6SKnyh870r8Ki6Py0CiO3fcTNWaCBU3E8FVDr7ZPRjbcDLHO30N/TmazdLk+JFMxVoZh6errUrcmnDQp5o4MocrI4o3N6dmXhp1hoHkOFV2R5CXtVwm3Qc0aBip8Z6lY0HtRpJ8GYz5pVFgxgkaHiaCuDE1gfOAhFdNbJIKxplCKNJqqyoqi0CT9tp9/IyyPE2SryYyDKD9LVKxKUqXbuFOM+yVDN/Rq+0ia1mLmtYNqK8rhTiSpLLNbLkDLuZvQ0X8QBoG+//5fIMjP4AQ/kJkuM+vW+sS1wkgiVSTi0Fq2XqoLFfFYMMkyHSFL2mOpHQmy+aU4xXHoLk6rrIkYiE1JNpZOJjO1ivduOLSkZeuk6/YBwR54jaVv6chXpmZQmJnEssveQjwVcPCXv1IWt4//sUVB7K4WpGTREqhvJCrO5MhtGLMTKWU5pUSpDKs1glhbB4W3VCSpTM6gOl2GQzxJt+RQUMFcOoENrXG0FEhESSvMmIVIZ6uaHL9QZn6Y067VNJueV4bdmYDdktJ7pAJNKKfG+pG/cz8GH/gfGLIARF4o9fs8RWSrUmZxN7Z+9za0sooTPiRuI22bsUMHsevWW1B+iFnYdOgqFWTPPxWnXPdxtK5eV8fB9IH92Pn1m1hz7MQh00Xm/C34+K23MiOXsPvLX8bgbXej5bz1OPs7tzIhduHgnXfghX/8OplEsr6U4ZtV9G69HMvf8wEkKUfgaUeWbs4zX/8Sxm/+AbzxCRVF1VpFM9hrvS2ZmZbuRUh2LpxPw7t60EWK8vgHPoCZ5w4i1pvBps99Bu2nbJ73XLyzB4kvLcAPt27F2LFR9MTjSKbb1L1h4mIq4iNL14u2ZRFJysazZCNHqA0DZXRcuBGnf+bz6v4JLDqVgk3r247DnMdJDkOzffJtDfoY2b0dg08/gbZlq7BiyyWk+MuQ2bAGU9v2snTtQnxBj3pu9OnfYXr/Hiq1EZ0bz0ZsUS+sFUvgDB+DFfh1v3X9Kg4xknfLRNZ21h2/RYTX29avU0pUSwUcuP07KLw0oBZrA5bGozt2MlA5updgzGuJnYyp6rt7778HP37fX+OJW77ZaKzKoo5eOdfRhMehO3+EbXzu8H/dXW/SOTwj0gZqeoVck+h3xES9LDjpVp3QXeRdqSVLkDllrepy5oeHMPH0brq2qdteRmNJwj7pYKFVlr75YrztKw6ya9aFTzF8Tk+pBZrmXRGRdCsSLMiQbKlfE7PLrjarCcSSA0QZvQQevGKncnrfXpVwZTde3+XvqN9b8d4PYfuNn8O+b9zO56K6oGpOiMYreNfSc7eoE+FO00P334XJx3fQzM685zd8/Hqs/uCHGGEy9QEslaT0Cm9t7rVyYqnGWogEGIl+nqUTmyxwTj62HTs/91ks3XqN2u8VBLKZoVt14pe/42oc/O6dzB2+qnEMNGHEPHHbSfiSqloakGP7D7+Dpz79BfT6cRXu5rHatk51Nh9aEaOJu2mOZIf36uDu6EDi3PVoIQGV5efiwSG1Rjny8COY3P4sI1WM2HKx4bpPYdEFlzA9RMOlhCAsLJssYqGxRbcZI8//9MfIrliDvjPOwqqL/xwD996P6rY9zGHWPNMNPf8UJl9+Cdm169G9YWOdapjB8auShsJMc85YdekVWL7lQgroKHd68qMfRcAEu+lrX1GdSdmBKjQn0aOrU9lso5bK53uSLiyscNu10tAy66FganAQD9zwD6jM5ZBe2IeLbvoGWs5YofZQyfKXxbpejl133omfXfth7P/Fz8NRLbXgb0nGNe26GhGST5MzFmEYll2oCl+sd2IZCcWtTKxd6rokwdYVpyK9fB1z1KnIrD0NDt1WiNGB738X3kxJVapiWVmR5pCurc2iSaIkmNJ0Hr+9+WYkMu0YfHI7Dv9+J+766Eew8vSNiFP4WGsGBanhh6bw1K3fRjSdwfSel5FikTT67At4+t9vgVssojA0Rp6VwOyhfjx9262qABrfw1KaJW15YprXvsVcEG1sT5eCji40fXSURVyAvTd9TSmv6nTVifQx/uwzmHiU7kb3Clu+GC27MsY247p07+SihN0m/Kgc6EXRIjmMgDvCF9mcsXJxDgniZSnN3xFLIcc6Yormd1mhCX2QpWc7SteolNUpNUQkIUvJpDkUrsrfqy1L8ZjaFSTrJKLsCbvz6BqxaBwdBReWbJmF3kTa2NYRVYFGHEYKqqFKFXtzMg6uUhaJyzZyQ/d/FdUm8LwmAuYwO/vhQBU+m+ddmy+NpBKNWpIzF7EdRSxrOygMMl6LruUw2tQXOTy1akNFk/XtU/V70H3g6YyNNk5GtOIp/DYvlKp9LoJLWuIl2fADfJ/X71PQ8Jo2Vzbv620OAFI9jtIqCQ7tnfC/JxhNT4dShds4UKvB66s1ftPnRqOh/l13hDDqWGhxqUgTsIV1Fzg5Y7TEpKsK+B/w+sdqUWuqv1CxUN8K/MqHLMnhj/g/J/4/juDky9VSg0kh/zQj322897Pao/8nwAC+AZicLeuzngAAAABJRU5ErkJggg==); background-repeat: no-repeat; background-position: top left; height: 64px; } + #header h1, #header h2 {margin: 0} + #header h2 { color: #888; font-weight: normal; font-size: 16px; } - #about h3 { margin: 0; margin-bottom: 10px; @@ -84,18 +84,26 @@ margin-left: -55px; margin-right: -10px; } + #about-content table { margin-top: 10px; margin-bottom: 10px; font-size: 11px; border-collapse: collapse; } + #about-content td { padding: 10px; padding-top: 3px; padding-bottom: 3px; } - #about-content td.name {color: #555} + + #about-content td.name { + font-weight: bold; + vertical-align: top; + color: #555; + } + #about-content td.value {color: #000} #about-content ul { @@ -107,21 +115,23 @@ background-color: #fcc; border: 1px solid #f00; } + #about-content.failure p { margin: 0; padding: 10px; } - #getting-started { border-top: 1px solid #ccc; margin-top: 25px; padding-top: 15px; } + #getting-started h1 { margin: 0; font-size: 20px; } + #getting-started h2 { margin: 0; font-size: 14px; @@ -129,40 +139,46 @@ color: #333; margin-bottom: 25px; } + #getting-started ol { margin-left: 0; padding-left: 0; } + #getting-started li { font-size: 18px; color: #888; margin-bottom: 25px; } + #getting-started li h2 { margin: 0; font-weight: normal; font-size: 18px; color: #333; } + #getting-started li p { color: #555; font-size: 13px; } - #sidebar ul { margin-left: 0; padding-left: 0; } + #sidebar ul h3 { margin-top: 25px; font-size: 16px; padding-bottom: 10px; border-bottom: 1px solid #ccc; } + #sidebar li { list-style-type: none; } + #sidebar ul.links li { margin-bottom: 5px; } @@ -221,7 +237,7 @@ <ol> <li> - <h2>Use <code>rails generate</code> to create your models and controllers</h2> + <h2>Use <code>bin/rails generate</code> to create your models and controllers</h2> <p>To see all available options, run it without parameters.</p> </li> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index c837fadb40..40a1915b54 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -21,6 +21,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 diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 285e2ce846..d836c0d6d6 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -3,32 +3,27 @@ require 'rails/test_unit/sub_test_task' task default: :test -desc 'Runs test:units, test:functionals, test:generators, test:integration together' +desc "Runs all tests in test folder" task :test do Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task end namespace :test do task :prepare do - # Placeholder task for other Railtie and plugins to enhance. See Active Record for an example. + # Placeholder task for other Railtie and plugins to enhance. + # If used with Active Record, this task runs before the database schema is synchronized. end - task :run => ['test:units', 'test:functionals', 'test:generators', 'test:integration'] - - # Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html - desc "Run tests quickly by merging all types and not resetting db" - Rails::TestTask.new(:all) do |t| + Rails::TestTask.new(:run) do |t| t.pattern = "test/**/*_test.rb" end - namespace :all do - desc "Run tests quickly, but also reset db" - task :db => %w[db:test:prepare test:all] - end + 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"].each do |name| + ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| Rails::TestTask.new(name => "test:prepare") do |t| t.pattern = "test/#{name}/**/*_test.rb" end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 56b8736800..7a1c897e3d 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 = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 0749615d03..ab8883e135 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,10 @@ def jruby_skip(message = '') end class ActiveSupport::TestCase + include ActiveSupport::Testing::Stream + # 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 end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 8f091cfdbf..0a2f283cce 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' @@ -225,7 +224,7 @@ module ApplicationTests test "assets do not require any assets group gem when manifest file is present" do app_file "app/assets/javascripts/application.js", "alert();" - add_to_env_config "production", "config.serve_static_assets = true" + add_to_env_config "production", "config.serve_static_files = true" ENV["RAILS_ENV"] = "production" precompile! diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f8b4ee30d8..8f5b2d0d68 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -41,7 +41,7 @@ module ApplicationTests def setup build_app boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") + supress_default_config end def teardown @@ -49,6 +49,15 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end + def supress_default_config + FileUtils.mv("#{app_path}/config/environments", "#{app_path}/config/__environments__") + end + + def restore_default_config + FileUtils.rm_rf("#{app_path}/config/environments") + FileUtils.mv("#{app_path}/config/__environments__", "#{app_path}/config/environments") + end + test "Rails.env does not set the RAILS_ENV environment variable which would leak out into rake tasks" do require "rails" @@ -59,6 +68,20 @@ module ApplicationTests end end + test "lib dir is on LOAD_PATH during config" do + app_file 'lib/my_logger.rb', <<-RUBY + require "logger" + class MyLogger < ::Logger + end + RUBY + add_to_top_of_config <<-RUBY + require 'my_logger' + config.logger = MyLogger.new STDOUT + RUBY + require "#{app_path}/config/environment" + assert_equal 'MyLogger', Rails.application.config.logger.class.name + end + test "a renders exception on pending migration" do add_to_config <<-RUBY config.active_record.migration_error = :page_load @@ -182,7 +205,7 @@ module ApplicationTests test "application is always added to eager_load namespaces" do require "#{app_path}/config/application" - assert Rails.application, Rails.application.config.eager_load_namespaces + assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application end test "the application can be eager loaded even when there are no frameworks" do @@ -266,10 +289,41 @@ module ApplicationTests assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path end + test "In production mode, config.serve_static_files is off by default" do + restore_default_config + + with_rails_env "production" do + require "#{app_path}/config/environment" + assert_not app.config.serve_static_files + end + end + + test "In production mode, config.serve_static_files is enabled when RAILS_SERVE_STATIC_FILES is set" do + restore_default_config + + with_rails_env "production" do + switch_env "RAILS_SERVE_STATIC_FILES", "1" do + require "#{app_path}/config/environment" + assert app.config.serve_static_files + end + end + end + + test "In production mode, config.serve_static_files is disabled when RAILS_SERVE_STATIC_FILES is blank" do + restore_default_config + + with_rails_env "production" do + switch_env "RAILS_SERVE_STATIC_FILES", " " do + require "#{app_path}/config/environment" + assert_not app.config.serve_static_files + end + end + end + test "Use key_generator when secret_key_base is set" do - make_basic_app do |app| - app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' - app.config.session_store :disabled + make_basic_app do |application| + application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.config.session_store :disabled end class ::OmgController < ActionController::Base @@ -287,9 +341,9 @@ module ApplicationTests end test "application verifier can be used in the entire application" do - make_basic_app do |app| - app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' - app.config.session_store :disabled + make_basic_app do |application| + application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.config.session_store :disabled end message = app.message_verifier(:sensitive_value).generate("some_value") @@ -301,10 +355,55 @@ module ApplicationTests assert_equal 'some_value', verifier.verify(message) end + test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + YAML + require "#{app_path}/config/environment" + + + assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator + assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator + message = app.message_verifier(:sensitive_value).generate("some_value") + assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message) + end + + test "warns when secrets.secret_key_base is blank and config.secret_token is set" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + YAML + require "#{app_path}/config/environment" + + assert_deprecated(/You didn't set `secret_key_base`./) do + app.env_config + end + end + + test "prefer secrets.secret_token over config.secret_token" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_token: 3b7cd727ee24e8444053437c36cc66c3 + YAML + require "#{app_path}/config/environment" + + assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token + end + test "application verifier can build different verifiers" do - make_basic_app do |app| - app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' - app.config.session_store :disabled + make_basic_app do |application| + application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.config.session_store :disabled end default_verifier = app.message_verifier(:sensitive_value) @@ -341,6 +440,21 @@ module ApplicationTests assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base end + test "config.secret_token over-writes a blank secrets.secret_token" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + secret_token: + YAML + require "#{app_path}/config/environment" + + assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token + assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token + end + test "custom secrets saved in config/secrets.yml are loaded in app secrets" do app_file 'config/secrets.yml', <<-YAML development: @@ -362,6 +476,51 @@ module ApplicationTests assert_nil app.secrets.not_defined end + test "config.secret_key_base over-writes a blank secrets.secret_key_base" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_key_base = "iaminallyoursecretkeybase" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + YAML + require "#{app_path}/config/environment" + + assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base + end + + test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + YAML + require "#{app_path}/config/environment" + + assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token + assert_equal nil, app.secrets.secret_key_base + assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator + end + + test "uses ActiveSupport::LegacyKeyGenerator with config.secret_token as app.key_generator when secrets.secret_key_base is blank" do + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_token = "" + RUBY + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: + YAML + require "#{app_path}/config/environment" + + assert_equal '', app.config.secret_token + assert_equal nil, app.secrets.secret_key_base + assert_raise ArgumentError, /\AA secret is required/ do + app.key_generator + end + end + test "protect from forgery is the default in a new app" do make_basic_app @@ -375,6 +534,45 @@ module ApplicationTests assert last_response.body =~ /csrf\-param/ end + test "default form builder specified as a string" do + + app_file 'config/initializers/form_builder.rb', <<-RUBY + class CustomFormBuilder < ActionView::Helpers::FormBuilder + def text_field(attribute, *args) + label(attribute) + super(attribute, *args) + end + end + Rails.configuration.action_view.default_form_builder = "CustomFormBuilder" + RUBY + + app_file 'app/models/post.rb', <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + + app_file 'app/controllers/posts_controller.rb', <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_for(Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + require "#{app_path}/config/environment" + + get "/posts" + assert_match(/label/, last_response.body) + end + test "default method for update can be changed" do app_file 'app/models/post.rb', <<-RUBY class Post @@ -429,8 +627,8 @@ module ApplicationTests end test "request forgery token param can be changed" do - make_basic_app do - app.config.action_controller.request_forgery_protection_token = '_xsrf_token_here' + make_basic_app do |application| + application.config.action_controller.request_forgery_protection_token = '_xsrf_token_here' end class ::OmgController < ActionController::Base @@ -449,8 +647,8 @@ module ApplicationTests end test "sets ActionDispatch::Response.default_charset" do - make_basic_app do |app| - app.config.action_dispatch.default_charset = "utf-16" + make_basic_app do |application| + application.config.action_dispatch.default_charset = "utf-16" end assert_equal "utf-16", ActionDispatch::Response.default_charset @@ -631,8 +829,8 @@ module ApplicationTests end test "config.action_dispatch.show_exceptions is sent in env" do - make_basic_app do |app| - app.config.action_dispatch.show_exceptions = true + make_basic_app do |application| + application.config.action_dispatch.show_exceptions = true end class ::OmgController < ActionController::Base @@ -793,8 +991,8 @@ module ApplicationTests end test "config.action_dispatch.ignore_accept_header" do - make_basic_app do |app| - app.config.action_dispatch.ignore_accept_header = true + make_basic_app do |application| + application.config.action_dispatch.ignore_accept_header = true end class ::OmgController < ActionController::Base @@ -831,9 +1029,9 @@ module ApplicationTests test "config.session_store with :active_record_store with activerecord-session_store gem" do begin - make_basic_app do |app| + make_basic_app do |application| ActionDispatch::Session::ActiveRecordStore = Class.new(ActionDispatch::Session::CookieStore) - app.config.session_store :active_record_store + application.config.session_store :active_record_store end ensure ActionDispatch::Session.send :remove_const, :ActiveRecordStore @@ -842,16 +1040,16 @@ module ApplicationTests test "config.session_store with :active_record_store without activerecord-session_store gem" do assert_raise RuntimeError, /activerecord-session_store/ do - make_basic_app do |app| - app.config.session_store :active_record_store + make_basic_app do |application| + application.config.session_store :active_record_store end end end test "config.log_level with custom logger" do - make_basic_app do |app| - app.config.logger = Logger.new(STDOUT) - app.config.log_level = :info + make_basic_app do |application| + application.config.logger = Logger.new(STDOUT) + application.config.log_level = :info end assert_equal Logger::INFO, Rails.logger.level end @@ -881,8 +1079,8 @@ module ApplicationTests end test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do - make_basic_app do |app| - app.config.annotations.register_extensions("coffee") do |tag| + make_basic_app do |application| + application.config.annotations.register_extensions("coffee") do |tag| /#\s*(#{tag}):?\s*(.*)$/ end end @@ -970,7 +1168,7 @@ module ApplicationTests app_file 'config/environments/development.rb', <<-RUBY Rails.application.configure do - config.paths.add 'config/database', with: 'config/nonexistant.yml' + config.paths.add 'config/database', with: 'config/nonexistent.yml' config.paths['config/database'] << 'config/database.yml' end RUBY diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 2d45c9b53f..97b51911d9 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -65,7 +65,6 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert Foo.method_defined?(:foo_path) assert Foo.method_defined?(:foo_url) assert Foo.method_defined?(:main_app) end diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 4f30f30f95..85066210f3 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -33,6 +33,35 @@ class LoadingTest < ActiveSupport::TestCase assert_equal 'omg', p.title end + test "concerns in app are autoloaded" do + app_file "app/controllers/concerns/trackable.rb", <<-CONCERN + module Trackable + end + CONCERN + + app_file "app/mailers/concerns/email_loggable.rb", <<-CONCERN + module EmailLoggable + end + CONCERN + + app_file "app/models/concerns/orderable.rb", <<-CONCERN + module Orderable + end + CONCERN + + app_file "app/validators/concerns/matchable.rb", <<-CONCERN + module Matchable + end + CONCERN + + require "#{rails_root}/config/environment" + + assert_nothing_raised(NameError) { Trackable } + assert_nothing_raised(NameError) { EmailLoggable } + assert_nothing_raised(NameError) { Orderable } + assert_nothing_raised(NameError) { Matchable } + end + test "models without table do not panic on scope definitions when loaded" do app_file "app/models/user.rb", <<-MODEL class User < ActiveRecord::Base diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 55e917c3ec..1752a9f3c6 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -40,6 +40,17 @@ module ApplicationTests assert_equal 404, last_response.status end + test "/rails/mailers is accessible with globbing route present" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '*foo', to: 'foo#index' + end + RUBY + app("development") + get "/rails/mailers" + assert_equal 200, last_response.status + end + test "mailer previews are loaded from the default preview_path" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base @@ -417,58 +428,6 @@ module ApplicationTests assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body end - test "*_path helpers emit a deprecation" do - - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get 'foo', to: 'foo#index' - end - RUBY - - mailer 'notifier', <<-RUBY - class Notifier < ActionMailer::Base - default from: "from@example.com" - - def path_in_view - mail to: "to@example.org" - end - - def path_in_mailer - @url = foo_path - mail to: "to@example.org" - end - end - RUBY - - html_template 'notifier/path_in_view', "<%= link_to 'foo', foo_path %>" - - mailer_preview 'notifier', <<-RUBY - class NotifierPreview < ActionMailer::Preview - def path_in_view - Notifier.path_in_view - end - - def path_in_mailer - Notifier.path_in_mailer - end - end - RUBY - - app('development') - - assert_deprecated do - get "/rails/mailers/notifier/path_in_view.html" - assert_equal 200, last_response.status - end - - html_template 'notifier/path_in_mailer', "No ERB in here" - - assert_deprecated do - get "/rails/mailers/notifier/path_in_mailer.html" - assert_equal 200, last_response.status - end - end - private def build_app super diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index b4db840e68..c951dabd6c 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -81,8 +81,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_header" - assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] - assert_equal "max-age=10, public", last_response.headers["Cache-Control"] + assert_equal "miss, store", last_response.headers["X-Rack-Cache"] + assert_equal "max-age=10, public", last_response.headers["Cache-Control"] body = last_response.body @@ -115,8 +115,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_etag" - assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] - assert_equal "public", last_response.headers["Cache-Control"] + assert_equal "miss, store", last_response.headers["X-Rack-Cache"] + assert_equal "public", last_response.headers["Cache-Control"] body = last_response.body etag = last_response.headers["ETag"] @@ -149,8 +149,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_last_modified" - assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] - assert_equal "public", last_response.headers["Cache-Control"] + assert_equal "miss, store", last_response.headers["X-Rack-Cache"] + assert_equal "public", last_response.headers["Cache-Control"] body = last_response.body last = last_response.headers["Last-Modified"] diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index 42096cfec4..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' @@ -60,6 +59,21 @@ module ApplicationTests assert_equal "YOU FAILED BRO", last_response.body end + test "url generation error when action_dispatch.show_exceptions is set raises an exception" do + controller :foo, <<-RUBY + class FooController < ActionController::Base + def index + raise ActionController::UrlGenerationError + end + end + RUBY + + app.config.action_dispatch.show_exceptions = true + + get '/foo' + assert_equal 500, last_response.status + end + test "unspecified route when action_dispatch.show_exceptions is not set raises an exception" do app.config.action_dispatch.show_exceptions = false diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index eb791f5687..dc96480d6d 100644 --- a/railties/test/application/middleware/sendfile_test.rb +++ b/railties/test/application/middleware/sendfile_test.rb @@ -61,7 +61,7 @@ module ApplicationTests test "files handled by ActionDispatch::Static are handled by Rack::Sendfile" do make_basic_app do |app| app.config.action_dispatch.x_sendfile_header = 'X-Sendfile' - app.config.serve_static_assets = true + app.config.serve_static_files = true app.paths["public"] = File.join(rails_root, "public") end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 31a64c2f5a..a8dc79d10a 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' @@ -203,7 +202,7 @@ module ApplicationTests RUBY add_to_config <<-RUBY - config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" RUBY require "#{app_path}/config/environment" @@ -258,7 +257,7 @@ module ApplicationTests RUBY add_to_config <<-RUBY - config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" RUBY require "#{app_path}/config/environment" @@ -317,7 +316,7 @@ module ApplicationTests RUBY add_to_config <<-RUBY - config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" secrets.secret_key_base = nil RUBY @@ -334,7 +333,7 @@ module ApplicationTests get '/foo/read_signed_cookie' assert_equal '2', last_response.body - verifier = ActiveSupport::MessageVerifier.new(app.config.secret_token) + verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token) get '/foo/read_raw_cookie' assert_equal 2, verifier.verify(last_response.body)['foo'] diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb index 0a793f8f60..121c5d3321 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' diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index a905598d80..04bd19784a 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -94,20 +94,27 @@ module ApplicationTests assert !middleware.include?("ActiveRecord::Migration::CheckPending") end - test "removes lock if cache classes is set" do + test "includes lock if cache_classes is set but eager_load is not" do add_to_config "config.cache_classes = true" boot! + assert middleware.include?("Rack::Lock") + end + + test "does not include lock if cache_classes is set and so is eager_load" do + add_to_config "config.cache_classes = true" + add_to_config "config.eager_load = true" + boot! assert !middleware.include?("Rack::Lock") end - test "removes lock if allow concurrency is set" do + test "does not include lock if allow_concurrency is set" do add_to_config "config.allow_concurrency = true" boot! assert !middleware.include?("Rack::Lock") end - test "removes static asset server if serve_static_assets is disabled" do - add_to_config "config.serve_static_assets = false" + test "removes static asset server if serve_static_files is disabled" do + add_to_config "config.serve_static_files = false" boot! assert !middleware.include?("ActionDispatch::Static") end @@ -118,6 +125,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 9ebf163671..cddc79cc85 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -8,6 +8,7 @@ module ApplicationTests build_app(initializers: true) boot_rails require "#{rails_root}/config/environment" + Rails.application.config.some_setting = 'something_or_other' end def teardown @@ -18,7 +19,7 @@ module ApplicationTests clone = Rails.application.clone assert_equal Rails.application.config, clone.config, "The cloned application should get a copy of the config" - assert_equal Rails.application.config.secret_key_base, clone.config.secret_key_base, "The base secret key on the config should be the same" + assert_equal Rails.application.config.some_setting, clone.config.some_setting, "The some_setting on the config should be the same" end def test_inheriting_multiple_times_from_application @@ -160,13 +161,14 @@ module ApplicationTests def test_inserting_configuration_into_application app = AppTemplate::Application.new(config: Rails.application.config) - new_config = Rails::Application::Configuration.new("root_of_application") - new_config.secret_key_base = "some_secret_key_dude" - app.config.secret_key_base = "a_different_secret_key" + app.config.some_setting = "a_different_setting" + assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set." - assert_equal "a_different_secret_key", app.config.secret_key_base, "The configuration's secret key should be set." + new_config = Rails::Application::Configuration.new("root_of_application") + new_config.some_setting = "some_setting_dude" app.config = new_config - assert_equal "some_secret_key_dude", app.config.secret_key_base, "The configuration's secret key should have changed." + + assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting should have changed." assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root." assert_equal new_config, app.config, "The application's config should have changed to the new config." end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 267469b6f5..c414732f92 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -109,6 +109,16 @@ module ApplicationTests db_fixtures_load database_url_db_name end + test 'db:fixtures:load with namespaced fixture' do + require "#{app_path}/config/environment" + Dir.chdir(app_path) do + `rails generate model admin::book title:string; + bundle exec rake db:migrate db:fixtures:load` + require "#{app_path}/app/models/admin/book" + assert_equal 2, Admin::Book.count + end + end + def db_structure_dump_and_load(expected_database) Dir.chdir(app_path) do `rails generate model book title:string; @@ -146,6 +156,31 @@ module ApplicationTests end end + test 'db:schema:load and db:structure:load do not purge the existing database' do + Dir.chdir(app_path) do + `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` + + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table(:comments) {} + end + RUBY + + list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } + + assert_equal '["posts"]', list_tables[] + `bin/rake db:schema:load` + assert_equal '["posts", "comments", "schema_migrations"]', list_tables[] + + app_file 'db/structure.sql', <<-SQL + CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255)); + SQL + + `bin/rake db:structure:load` + assert_equal '["posts", "comments", "schema_migrations", "users"]', list_tables[] + end + end + def db_test_load_structure Dir.chdir(app_path) do `rails generate model book title:string; @@ -165,15 +200,6 @@ module ApplicationTests db_test_load_structure end - test 'db:test deprecation' do - require "#{app_path}/config/environment" - Dir.chdir(app_path) do - output = `bundle exec rake db:migrate db:test:prepare 2>&1` - assert_equal "WARNING: db:test:prepare is deprecated. The Rails test helper now maintains " \ - "your test schema automatically, see the release notes for details.\n", output - end - end - test 'db:setup loads schema and seeds database' do begin @old_rails_env = ENV["RAILS_ENV"] diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index e8c8de9f73..2bff21dae5 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -194,7 +194,10 @@ module ApplicationTests assert_no_match(/Errors running/, output) end - def test_scaffold_with_references_columns_tests_pass_by_default + 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` @@ -227,7 +230,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 diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 8576a2b738..cbada6be97 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -123,6 +123,26 @@ module ApplicationTests assert_equal '/archives', last_response.body end + test "mount named rack app" do + controller :foo, <<-RUBY + class FooController < ApplicationController + def index + render text: my_blog_path + end + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog", as: "my_blog" + get '/foo' => 'foo#index' + end + RUBY + + get '/foo' + assert_equal '/blog', last_response.body + end + test "multiple controllers" do controller :foo, <<-RUBY class FooController < ApplicationController diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 118f22995e..a12f3cfc24 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -47,9 +47,8 @@ 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 @@ -109,6 +108,17 @@ module ApplicationTests end end + def test_run_jobs + create_test_file :jobs, 'foo_job' + create_test_file :jobs, 'bar_job' + create_test_file :models, 'foo' + run_test_jobs_command.tap do |output| + assert_match "FooJobTest", output + assert_match "BarJobTest", output + assert_match "2 runs, 2 assertions, 0 failures", output + end + end + def test_run_functionals create_test_file :mailers, 'foo_mailer' create_test_file :controllers, 'bar_controller' @@ -132,11 +142,11 @@ module ApplicationTests end def test_run_all_suites - suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration] + suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs] suites.each { |suite| create_test_file suite, "foo_#{suite}" } run_test_command('') .tap do |output| suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output } - assert_match "7 runs, 7 assertions, 0 failures", output + assert_match "8 runs, 8 assertions, 0 failures", output end end @@ -245,7 +255,7 @@ module ApplicationTests def run_test_command(arguments = 'test/unit/test_test.rb') run_task ['test', arguments] end - %w{ mailers models helpers units controllers functionals integration }.each do |type| + %w{ mailers models helpers units controllers functionals integration jobs }.each do |type| define_method("run_test_#{type}_command") do run_task ["test:#{type}"] end @@ -285,15 +295,6 @@ module ApplicationTests app_file 'db/schema.rb', '' end - def redirect_stderr(target_stream) - previous_stderr = STDERR.dup - $stderr.reopen(target_stream) - yield - target_stream.rewind - ensure - $stderr = previous_stderr - end - def create_test_file(path = :unit, name = 'test') app_file "test/#{path}/#{name}_test.rb", <<-RUBY require 'test_helper' diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index c724c867ec..c7132837b1 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -193,6 +193,98 @@ module ApplicationTests assert_successful_test_run('models/user_test.rb') end + # TODO: would be nice if we could detect the schema change automatically. + # For now, the user has to synchronize the schema manually. + # This test-case serves as a reminder for this use-case. + test "manually synchronize test schema after rollback" do + output = script('generate model user name:string') + version = output.match(/(\d+)_create_users\.rb/)[1] + + app_file 'test/models/user_test.rb', <<-RUBY + require 'test_helper' + + class UserTest < ActiveSupport::TestCase + test "user" do + assert_equal ["id", "name"], User.columns_hash.keys + end + end + RUBY + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: #{version}) do + create_table :users do |t| + t.string :name + end + end + RUBY + + assert_successful_test_run "models/user_test.rb" + + # Simulate `db:rollback` + edit of the migration file + `db:migrate` + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: #{version}) do + create_table :users do |t| + t.string :name + t.integer :age + end + end + RUBY + + assert_successful_test_run "models/user_test.rb" + + Dir.chdir(app_path) { `bin/rake db:test:prepare` } + + assert_unsuccessful_run "models/user_test.rb", <<-ASSERTION +Expected: ["id", "name"] + Actual: ["id", "name", "age"] + ASSERTION + end + + test "hooks for plugins" do + output = script('generate model user name:string') + version = output.match(/(\d+)_create_users\.rb/)[1] + + app_file 'lib/tasks/hooks.rake', <<-RUBY + task :before_hook do + has_user_table = ActiveRecord::Base.connection.table_exists?('users') + puts "before: " + has_user_table.to_s + end + + task :after_hook do + has_user_table = ActiveRecord::Base.connection.table_exists?('users') + puts "after: " + has_user_table.to_s + end + + Rake::Task["db:test:prepare"].enhance [:before_hook] do + Rake::Task[:after_hook].invoke + end + RUBY + app_file 'test/models/user_test.rb', <<-RUBY + require 'test_helper' + class UserTest < ActiveSupport::TestCase + test "user" do + User.create! name: "Jon" + end + end + RUBY + + # Simulate `db:migrate` + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: #{version}) do + create_table :users do |t| + t.string :name + end + end + RUBY + + output = assert_successful_test_run "models/user_test.rb" + assert_includes output, "before: false\nafter: true" + + # running tests again won't trigger a schema update + output = assert_successful_test_run "models/user_test.rb" + assert_not_includes output, "before:" + assert_not_includes output, "after:" + end + private def assert_unsuccessful_run(name, message) result = run_test_file(name) @@ -208,7 +300,7 @@ module ApplicationTests end def run_test_file(name, options = {}) - ruby '-Itest', "#{app_path}/test/#{name}", options + ruby '-Itest', "#{app_path}/test/#{name}", options.deep_merge(env: {"RAILS_ENV" => "test"}) end def ruby(*args) diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index efbc853d7b..ef16ab56ed 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -15,7 +15,7 @@ module ApplicationTests require "action_view/railtie" class MyApp < Rails::Application - config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" + secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log config.eager_load = false 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/commands/console_test.rb b/railties/test/commands/console_test.rb index 4aea3e980f..de0cf0ba9e 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -46,28 +46,6 @@ class Rails::ConsoleTest < ActiveSupport::TestCase assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end - if RUBY_VERSION < '2.0.0' - def test_debugger_option - console = Rails::Console.new(app, parse_arguments(["--debugger"])) - assert console.debugger? - end - - def test_no_options_does_not_set_debugger_flag - console = Rails::Console.new(app, parse_arguments([])) - assert !console.debugger? - end - - def test_start_with_debugger - stubbed_console = Class.new(Rails::Console) do - def require_debugger - end - end - - rails_console = stubbed_console.new(app, parse_arguments(["--debugger"])) - silence_stream(STDOUT) { rails_console.start } - end - end - def test_console_with_environment start ["-e production"] assert_match(/\sproduction\s/, output) diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 2206e389b5..c6de2c1fb9 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -129,7 +129,7 @@ class ActionsTest < Rails::Generators::TestCase run_generator action :environment do - '# This wont be added' + _ = '# This wont be added'# assignment to silence parse-time warning "unused literal ignored" '# This will be added' end @@ -222,14 +222,14 @@ class ActionsTest < Rails::Generators::TestCase 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") + assert_match "application up and running", action(:readme, "README.md") 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") + assert_no_match "application up and running", action(:readme, "README.md") end def test_log diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index d687a8f506..4c5dd70a88 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 @@ -33,6 +33,7 @@ DEFAULT_APP_FILES = %w( log test/test_helper.rb test/fixtures + test/fixtures/files test/controllers test/models test/helpers @@ -160,6 +161,38 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) end + def test_rails_update_does_not_create_callback_terminator_initializer + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.rm("#{app_root}/config/initializers/callback_terminator.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/callback_terminator.rb" + end + + def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.touch("#{app_root}/config/initializers/callback_terminator.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/callback_terminator.rb" + end + def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured app_root = File.join(destination_root, 'myapp') run_generator [app_root] @@ -176,6 +209,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!/ @@ -259,18 +324,42 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_without_skips + run_generator + assert_file "config/application.rb", /\s+require\s+["']rails\/all["']/ + assert_file "config/environments/development.rb" do |content| + assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content) + end + assert_file "config/environments/test.rb" do |content| + assert_match(/config\.action_mailer\.delivery_method = :test/, content) + end + assert_file "config/environments/production.rb" do |content| + assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + end + end + 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) end end - def test_generator_if_skip_action_view_is_given - run_generator [destination_root, "--skip-action-view"] - assert_file "config/application.rb", /#\s+require\s+["']action_view\/railtie["']/ + def test_generator_if_skip_action_mailer_is_given + run_generator [destination_root, "--skip-action-mailer"] + assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ + assert_file "config/environments/development.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "config/environments/test.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "config/environments/production.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end end def test_generator_if_skip_sprockets_is_given @@ -345,21 +434,20 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_inclusion_of_a_debugger run_generator - if defined?(JRUBY_VERSION) + if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" assert_file "Gemfile" do |content| assert_no_match(/byebug/, content) - assert_no_match(/debugger/, content) end - elsif RUBY_VERSION < '2.0.0' - assert_gem 'debugger' else assert_gem 'byebug' end end - def test_inclusion_of_doc + def test_inclusion_of_method_source run_generator - assert_file 'Gemfile', /gem 'sdoc',\s+'~> 0.4.0',\s+group: :doc/ + assert_file "Gemfile" do |content| + assert_gem 'method_source' + end end def test_template_from_dir_pwd @@ -386,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["']/ @@ -425,6 +513,24 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem 'web-console' end + def test_web_console_with_dev_option + run_generator [destination_root, "--dev"] + + assert_file "Gemfile" do |content| + assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) + assert_no_match(/gem 'web-console', '~> 2.0'/, content) + end + end + + def test_web_console_with_edge_option + run_generator [destination_root, "--edge"] + + assert_file "Gemfile" do |content| + assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) + assert_no_match(/gem 'web-console', '~> 2.0'/, content) + end + end + def test_spring run_generator assert_gem 'spring' @@ -455,12 +561,11 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_generator_if_skip_gems_is_given - run_generator [destination_root, "--skip-gems", "turbolinks", "coffee-rails"] + def test_generator_if_skip_turbolinks_is_given + run_generator [destination_root, "--skip-turbolinks"] assert_file "Gemfile" do |content| assert_no_match(/turbolinks/, content) - assert_no_match(/coffee-rails/, content) end assert_file "app/views/layouts/application.html.erb" do |content| assert_no_match(/data-turbolinks-track/, content) @@ -496,7 +601,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_psych_gem run_generator - gem_regex = /gem 'psych',\s+'~> 2.0', \s+platforms: :rbx/ + gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ assert_file "Gemfile" do |content| if defined?(Rubinius) diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb index c48bc20899..ee7c009305 100644 --- a/railties/test/generators/generated_attribute_test.rb +++ b/railties/test/generators/generated_attribute_test.rb @@ -141,4 +141,12 @@ class GeneratedAttributeTest < Rails::Generators::TestCase assert_equal "post_id", create_generated_attribute('references', 'post').column_name assert_equal "post_id", create_generated_attribute('belongs_to', 'post').column_name end + + def test_parse_required_attribute_with_index + att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index") + assert_equal "supplier", att.name + assert_equal :references, att.type + assert att.has_index? + assert att.required? + end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 6cc91f166b..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,11 +45,4 @@ module GeneratorsTestHelper FileUtils.cp routes, destination end - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end end diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb new file mode 100644 index 0000000000..a9e0cea94f --- /dev/null +++ b/railties/test/generators/job_generator_test.rb @@ -0,0 +1,29 @@ +require 'generators/generators_test_helper' +require 'rails/generators/job/job_generator' + +class JobGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + 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) + 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(/queue_as :important/, job) + end + end + + 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(/queue_as :admin/, job) + end + end +end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 25649881eb..584e7a82aa 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -7,94 +7,114 @@ 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 < ActionMailer::Base/, 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 + end + + def test_application_mailer_skeleton_is_created + run_generator + assert_file "app/mailers/application_mailer.rb" do |mailer| + assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) assert_match(/default from: "from@example.com"/, mailer) + assert_match(/layout 'mailer'/, mailer) end end 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_file "test/mailers/previews/notifier_mailer_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(/class NotifierMailerPreview < ActionMailer::Preview/, preview) assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/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_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 + + assert_file "app/views/layouts/mailer.text.erb" do |view| + assert_match(/<%= yield %>/, view) + end end 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 + + assert_file "app/views/layouts/mailer.html.erb" do |view| + assert_match(%r{<html>\n <body>\n <%= yield %>\n </body>\n</html>}, view) + end end 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 def test_logs_if_the_template_engine_cannot_be_found @@ -104,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 < ActionMailer::Base/, 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_file "test/mailers/previews/farm/animal_mailer_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(/class Farm::AnimalMailerPreview < ActionMailer::Preview/, preview) assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/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) @@ -132,4 +152,35 @@ class MailerGeneratorTest < Rails::Generators::TestCase end end end + + def test_mailer_on_revoke + run_generator + run_generator ["notifier"], behavior: :revoke + + assert_no_file "app/mailers/notifier.rb" + assert_no_file "app/views/notifier/foo.text.erb" + assert_no_file "app/views/notifier/bar.text.erb" + assert_no_file "app/views/notifier/foo.html.erb" + assert_no_file "app/views/notifier/bar.html.erb" + + assert_file "app/mailers/application_mailer.rb" + 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 72f5fe29ca..57bc220558 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -85,6 +85,19 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_remove_migration_with_references_removes_foreign_keys + migration = "remove_references_from_books" + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/remove_reference :books, :author,.*\sforeign_key: true/, change) + assert_match(/remove_reference :books, :distributor/, change) # sanity check + assert_no_match(/remove_reference :books, :distributor,.*\sforeign_key: true/, change) + end + end + end + def test_add_migration_with_attributes_and_indices migration = "add_title_with_index_and_body_to_posts" run_generator [migration, "title:string:index", "body:text", "user_id:integer:uniq"] @@ -171,6 +184,19 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_with_references_adds_foreign_keys + migration = "add_references_to_books" + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_reference :books, :author,.*\sforeign_key: true/, change) + assert_match(/add_reference :books, :distributor/, change) # sanity check + assert_no_match(/add_reference :books, :distributor,.*\sforeign_key: true/, change) + end + end + end + def test_create_join_table_migration migration = "add_media_join_table" run_generator [migration, "artist_id", "musics:uniq"] @@ -205,7 +231,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end end - + def test_properly_identifies_usage_file assert generator_class.send(:usage_path) end @@ -250,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 c78597c81b..17a13fbf1f 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -223,7 +223,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_timestamps run_generator - assert_migration "db/migrate/create_accounts.rb", /t.timestamps null: false/ + assert_migration "db/migrate/create_accounts.rb", /t.timestamps/ end def test_migration_timestamps_are_skipped @@ -287,18 +287,18 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_fixtures_use_the_references_ids run_generator ["LineItem", "product:references", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product_id: \n cart_id: / + assert_file "test/fixtures/line_items.yml", /product: \n cart: / assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product_id"=>nil, "cart_id"=>nil}, "two"=>{"product_id"=>nil, "cart_id"=>nil}}) + {"one"=>{"product"=>nil, "cart"=>nil}, "two"=>{"product"=>nil, "cart"=>nil}}) end def test_fixtures_use_the_references_ids_and_type run_generator ["LineItem", "product:references{polymorphic}", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product_id: \n product_type: Product\n cart_id: / + assert_file "test/fixtures/line_items.yml", /product: \n product_type: Product\n cart: / assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product_id"=>nil, "product_type"=>"Product", "cart_id"=>nil}, - "two"=>{"product_id"=>nil, "product_type"=>"Product", "cart_id"=>nil}}) + {"one"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}, + "two"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}}) end def test_fixtures_respect_reserved_yml_keywords @@ -407,6 +407,48 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_foreign_key_is_not_added_for_non_references + run_generator ["account", "supplier:string"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_no_match(/foreign_key/, up) + end + end + end + + def test_foreign_key_is_added_for_references + run_generator ["account", "supplier:belongs_to", "user:references"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.belongs_to :supplier,.*\sforeign_key: true/, up) + assert_match(/t\.references :user,.*\sforeign_key: true/, up) + end + end + end + + def test_foreign_key_is_skipped_for_polymorphic_references + run_generator ["account", "supplier:belongs_to{polymorphic}"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_no_match(/foreign_key/, up) + end + 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..18a26fde05 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -2,16 +2,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 - class NamedBaseTest < Rails::Generators::TestCase include GeneratorsTestHelper tests Rails::Generators::ScaffoldControllerGenerator @@ -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 diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index 7eeb084eab..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 < ActionMailer::Base/, mailer) - assert_match(/default from: "from@example.com"/, 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 985644e8af..a0f244da28 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -28,11 +28,11 @@ 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 @@ -44,7 +44,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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/ @@ -54,7 +61,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator assert_file "README.rdoc", /Bukkits/ assert_no_file "config/routes.rb" - assert_file "test/test_helper.rb" + assert_file "test/test_helper.rb" do |content| + assert_match(/require.+test\/dummy\/config\/environment/, content) + assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content) + assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content) + end assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ end @@ -67,13 +78,10 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_inclusion_of_a_debugger run_generator [destination_root, '--full'] - if defined?(JRUBY_VERSION) + if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" assert_file "Gemfile" do |content| assert_no_match(/byebug/, content) - assert_no_match(/debugger/, content) end - elsif RUBY_VERSION < '2.0.0' - assert_file "Gemfile", /# gem 'debugger'/ else assert_file "Gemfile", /# gem 'byebug'/ end @@ -111,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 @@ -136,6 +144,20 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_app_generator_without_skips + run_generator + assert_file "test/dummy/config/application.rb", /\s+require\s+["']rails\/all["']/ + assert_file "test/dummy/config/environments/development.rb" do |content| + assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content) + end + assert_file "test/dummy/config/environments/test.rb" do |content| + assert_match(/config\.action_mailer\.delivery_method = :test/, content) + end + assert_file "test/dummy/config/environments/production.rb" do |content| + assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + end + end + def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ @@ -149,6 +171,20 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_action_mailer_is_removed_from_frameworks_if_skip_action_mailer_is_given + run_generator [destination_root, "--skip-action-mailer"] + assert_file "test/dummy/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ + assert_file "test/dummy/config/environments/development.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "test/dummy/config/environments/test.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "test/dummy/config/environments/production.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + end + def test_ensure_that_database_option_is_passed_to_app_generator run_generator [destination_root, "--database", "postgresql"] assert_file "test/dummy/config/database.yml", /postgres/ @@ -156,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 @@ -223,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 @@ -242,6 +310,67 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents) assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents) end + assert_file "test/test_helper.rb" do |content| + assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) + assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) + 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 @@ -270,6 +399,10 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/dummy" assert_file "spec/dummy/config/application.rb" assert_no_file "test/dummy" + assert_file "test/test_helper.rb" do |content| + assert_match(/require.+spec\/dummy\/config\/environment/, content) + assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content) + end end def test_creating_dummy_application_with_different_name @@ -277,13 +410,18 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/fake" assert_file "spec/fake/config/application.rb" assert_no_file "test/dummy" + assert_file "test/test_helper.rb" do |content| + assert_match(/require.+spec\/fake\/config\/environment/, content) + assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content) + end 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" + assert_no_file "test/test_helper.rb" assert_file '.gitignore' do |contents| assert_match(/spec\/dummy/, contents) end @@ -291,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..34e752cea1 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 diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 637bde2a44..ee06802874 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -58,15 +58,25 @@ 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 - %w(index edit new show _form).each do |view| + assert_no_file "app/views/layouts/product_lines.html.erb" + + %w(index show).each do |view| assert_file "app/views/product_lines/#{view}.html.erb" end - assert_no_file "app/views/layouts/product_lines.html.erb" + + %w(edit new).each do |view| + assert_file "app/views/product_lines/#{view}.html.erb", /render 'form', product_line: @product_line/ + end + + assert_file "app/views/product_lines/_form.html.erb" do |test| + assert_match 'product_line', test + assert_no_match '@product_line', test + end # Helpers assert_file "app/helpers/product_lines_helper.rb" @@ -83,8 +93,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase 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 @@ -235,6 +245,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" @@ -249,13 +282,27 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/assets/stylesheets/posts.css" end - def test_scaffold_generator_no_assets_with_switch_resource_route_false + def test_scaffold_generator_with_switch_resource_route_false run_generator [ "posts", "--resource-route=false" ] assert_file "config/routes.rb" do |route| assert_no_match(/resources :posts$/, route) end end + def test_scaffold_generator_no_helper_with_switch_no_helper + output = run_generator [ "posts", "--no-helper" ] + + assert_no_match(/error/, output) + assert_no_file "app/helpers/posts_helper.rb" + end + + def test_scaffold_generator_no_helper_with_switch_helper_false + output = run_generator [ "posts", "--helper=false" ] + + assert_no_match(/error/, output) + assert_no_file "app/helpers/posts_helper.rb" + end + def test_scaffold_generator_no_stylesheets run_generator [ "posts", "--no-stylesheets" ] assert_no_file "app/assets/stylesheets/scaffold.css" diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index b998fef42e..68f07f29d7 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 diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 40469e31d7..63209559d7 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 @@ -143,6 +145,7 @@ module TestHelpers config.active_support.deprecation = :log config.active_support.test_order = :random config.action_controller.allow_forgery_protection = false + config.log_level = :info RUBY end @@ -162,6 +165,8 @@ module TestHelpers app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" app.config.session_store :cookie_store, key: "_myapp_session" app.config.active_support.deprecation = :log + app.config.active_support.test_order = :random + app.config.log_level = :info yield app if block_given? app.initialize! @@ -231,6 +236,15 @@ module TestHelpers end end + def add_to_top_of_config(str) + environment = File.read("#{app_path}/config/application.rb") + if environment =~ /(Rails::Application\s*)/ + File.open("#{app_path}/config/application.rb", 'w') do |f| + f.puts $` + $1 + "\n#{str}\n" + $' + end + end + end + def add_to_config(str) environment = File.read("#{app_path}/config/application.rb") if environment =~ /(\n\s*end\s*end\s*)\Z/ @@ -263,12 +277,6 @@ module TestHelpers 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 @@ -297,35 +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 end # Create a scope and build a fixture rails app diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index 13bf29d3c3..e3dfcdfb7d 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'abstract_unit' require 'active_support/core_ext/object/with_options' require 'active_support/core_ext/object/json' diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 8d61af4972..25b0e761cb 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'mocha/setup' # FIXME: stop using mocha module ActionController class Base @@ -15,26 +14,26 @@ class InfoControllerTest < ActionController::TestCase get '/rails/info/properties' => "rails/info#properties" get '/rails/info/routes' => "rails/info#routes" end - @controller.stubs(:local_request? => true) @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 test "info controller does not allow remote requests" do - @controller.stubs(local_request?: false) + @request.env["REMOTE_ADDR"] = "example.org" get :properties assert_response :forbidden end test "info controller renders an error message when request was forbidden" do - @controller.stubs(local_request?: false) + @request.env["REMOTE_ADDR"] = "example.org" get :properties assert_select 'p' end test "info controller allows requests when all requests are considered local" do - @controller.stubs(local_request?: true) get :properties assert_response :success end diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 4bec302ff8..92e4af25b5 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -38,21 +38,10 @@ class InfoTest < ActiveSupport::TestCase end def test_rails_version - assert_property 'Rails version', + assert_property 'Rails version', File.read(File.realpath('../../../RAILS_VERSION', __FILE__)).chomp end - def test_framework_version - assert_property 'Active Support version', ActiveSupport.version.to_s - end - - def test_frameworks_exist - Rails::Info.frameworks.each do |framework| - dir = File.dirname(__FILE__) + "/../../" + framework.delete('_') - assert File.directory?(dir), "#{framework.classify} does not exist" - end - end - def test_html_includes_middleware Rails::Info.module_eval do property 'Middleware', ['Rack::Lock', 'Rack::Static'] diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 1976466229..6185742cc1 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -498,17 +498,12 @@ YAML boot_rails initializers = Rails.application.initializers.tsort - index = initializers.index { |i| i.name == "dummy_initializer" } - selection = initializers[(index-3)..(index)].map(&:name).map(&:to_s) + dummy_index = initializers.index { |i| i.name == "dummy_initializer" } + config_index = initializers.rindex { |i| i.name == :load_config_initializers } + stack_index = initializers.index { |i| i.name == :build_middleware_stack } - assert_equal %w( - load_config_initializers - load_config_initializers - engines_blank_point - dummy_initializer - ), selection - - assert index < initializers.index { |i| i.name == :build_middleware_stack } + assert config_index < dummy_index + assert dummy_index < stack_index end class Upcaser @@ -518,12 +513,12 @@ YAML def call(env) response = @app.call(env) - response[2].each { |b| b.upcase! } + response[2].each(&:upcase!) response end end - test "engine is a rack app and can have his own middleware stack" do + test "engine is a rack app and can have its own middleware stack" do add_to_config("config.action_dispatch.show_exceptions = false") @plugin.write "lib/bukkits.rb", <<-RUBY @@ -746,8 +741,8 @@ YAML assert_equal "bukkits_", Bukkits.table_name_prefix assert_equal "bukkits", Bukkits::Engine.engine_name assert_equal Bukkits.railtie_namespace, Bukkits::Engine - assert ::Bukkits::MyMailer.method_defined?(:foo_path) - assert !::Bukkits::MyMailer.method_defined?(:bar_path) + assert ::Bukkits::MyMailer.method_defined?(:foo_url) + assert !::Bukkits::MyMailer.method_defined?(:bar_url) get("/bukkits/from_app") assert_equal "false", last_response.body @@ -1210,7 +1205,7 @@ YAML test "engine can be properly mounted at root" do add_to_config("config.action_dispatch.show_exceptions = false") - add_to_config("config.serve_static_assets = false") + add_to_config("config.serve_static_files = false") @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits diff --git a/tasks/release.rb b/tasks/release.rb index de05dfad99..d8c1390eef 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties activejob ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer railties ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip @@ -102,7 +102,7 @@ namespace :all do abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" end - unless ENV['SKIP_TAG'] || `git tag | grep '^#{tag}$`.strip.empty? + unless ENV['SKIP_TAG'] || `git tag | grep '^#{tag}$'`.strip.empty? abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\ " been released? Git tagging can be skipped by setting SKIP_TAG=1" end diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000000..25ab798bd5 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,7 @@ +## Rails dev tools + +This is a collection of utilities used for Rails internal development. +They aren't used by Rails apps directly. + + * `console` drops you in irb and loads local Rails repos + * `profile` profiles `Kernel#require` to help reduce startup time diff --git a/tools/line_statistics b/tools/line_statistics index 5eb5cfdc3d..bfa921b095 100755..100644 --- a/tools/line_statistics +++ b/tools/line_statistics @@ -1,4 +1,5 @@ -#!/usr/bin/env ruby +# Class used to calculates LOC for a provided file list. +# # Example: # files = FileList["lib/active_record/**/*.rb"] # CodeTools::LineStatistics.new(files).print_loc diff --git a/tools/profile b/tools/profile index a35dd18b77..eb7fc7792b 100755 --- a/tools/profile +++ b/tools/profile @@ -1,4 +1,7 @@ #!/usr/bin/env ruby +# Profile require calls giving information about the time and the files that are called +# when loading the provided file. +# # Example: # tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer] ENV['NO_RELOAD'] ||= '1' @@ -65,7 +68,7 @@ module CodeTools private def assert_ruby_file_exists(path) - fail Error.new("No such file") unless File.exists?(path) + fail Error.new("No such file") unless File.exist?(path) fail Error.new("#{path} is a directory") if File.directory?(path) ruby_extension = File.extname(path) == '.rb' ruby_executable = File.open(path, 'rb') {|f| f.readline } =~ [/\A#!.*ruby/] diff --git a/version.rb b/version.rb index 672b78c599..7d74b1bfe5 100644 --- a/version.rb +++ b/version.rb @@ -5,10 +5,10 @@ module Rails end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |